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

wusheng 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 54b7a645c Fix sidx tag filter range check issue (#991)
54b7a645c is described below

commit 54b7a645c3c06b269f1e6709b5aa321110b4c274
Author: Gao Hongtao <[email protected]>
AuthorDate: Fri Mar 6 18:04:59 2026 +0800

    Fix sidx tag filter range check issue (#991)
---
 .github/actions/build-docker-image/action.yml      |   2 +-
 .github/actions/setup-build-env/action.yml         |   2 +-
 .github/workflows/e2e.yml                          |   4 +-
 .github/workflows/prepare.yml                      |   2 +-
 .github/workflows/property-repair.yml              |   2 +-
 .github/workflows/test-integration-distributed.yml |   2 +-
 .github/workflows/test-integration-standalone.yml  |   2 +-
 .github/workflows/test.yml                         |   2 +-
 CHANGES.md                                         |   1 +
 banyand/cmd/dump/sidx.go                           | 106 ++++++++++++++--
 banyand/internal/sidx/block.go                     |   8 +-
 banyand/internal/sidx/block_scanner.go             | 134 ++++++++++++++++++++-
 banyand/internal/sidx/block_scanner_test.go        |  97 +++++++++++++++
 banyand/internal/sidx/interfaces.go                |   4 +-
 banyand/internal/sidx/iter.go                      |   4 +-
 banyand/internal/sidx/iter_test.go                 |  14 +--
 banyand/internal/sidx/part_key_iter.go             |  30 ++++-
 banyand/internal/sidx/part_key_iter_test.go        |   8 +-
 banyand/internal/sidx/tag_filter_op.go             |  17 +--
 banyand/internal/sidx/tag_filter_op_test.go        | 115 +++++++++---------
 .../duration_range_and_ipv4_order_timestamp.ql     |  23 ++++
 .../duration_range_and_ipv4_order_timestamp.yml    |  50 ++++++++
 .../duration_range_and_ipv4_order_timestamp.yml    |  24 ++++
 test/cases/trace/trace.go                          |   2 +
 24 files changed, 541 insertions(+), 114 deletions(-)

diff --git a/.github/actions/build-docker-image/action.yml 
b/.github/actions/build-docker-image/action.yml
index 372a191f4..c2f6a62f4 100644
--- a/.github/actions/build-docker-image/action.yml
+++ b/.github/actions/build-docker-image/action.yml
@@ -54,7 +54,7 @@ runs:
         ls -lh banyandb-testing-image.tar.gz
 
     - name: Upload docker image artifact
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v7
       with:
         name: ${{ inputs.artifact-name }}
         path: banyandb-testing-image.tar.gz
diff --git a/.github/actions/setup-build-env/action.yml 
b/.github/actions/setup-build-env/action.yml
index ae6137fb2..6dc18bfef 100644
--- a/.github/actions/setup-build-env/action.yml
+++ b/.github/actions/setup-build-env/action.yml
@@ -32,7 +32,7 @@ runs:
   steps:
     - name: Download build artifacts
       if: inputs.download-artifacts == 'true'
-      uses: actions/download-artifact@v7
+      uses: actions/download-artifact@v8
       with:
         name: build-artifacts
 
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index 793dade91..60825a5e5 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -104,13 +104,13 @@ jobs:
           df -h
           du -sh .
           docker images
-      - uses: actions/upload-artifact@v4
+      - uses: actions/upload-artifact@v7
         if: ${{ failure() }}
         name: Upload Logs
         with:
           name: test-logs-${{ matrix.test.name }}
           path: "${{ env.SW_INFRA_E2E_LOG_DIR }}"
-      - uses: actions/upload-artifact@v4
+      - uses: actions/upload-artifact@v7
         if: ${{ (failure() && matrix.test.name == 'Lifecycle') }}
         name: Upload generated data
         with:
diff --git a/.github/workflows/prepare.yml b/.github/workflows/prepare.yml
index 281f171e9..1668911ff 100644
--- a/.github/workflows/prepare.yml
+++ b/.github/workflows/prepare.yml
@@ -55,7 +55,7 @@ jobs:
           npm run build
           cd ..
       - name: Upload generated files
-        uses: actions/upload-artifact@v6
+        uses: actions/upload-artifact@v7
         with:
           name: build-artifacts
           path: |
diff --git a/.github/workflows/property-repair.yml 
b/.github/workflows/property-repair.yml
index cd875dce5..2774dfb47 100644
--- a/.github/workflows/property-repair.yml
+++ b/.github/workflows/property-repair.yml
@@ -64,7 +64,7 @@ jobs:
         if: ${{ failure() }}
         id: sanitize-name
         run: echo "sanitized=$(echo '${{ inputs.test-name }}' | sed 
's/[^a-zA-Z0-9._-]/-/g')" >> $GITHUB_OUTPUT
-      - uses: actions/upload-artifact@v4
+      - uses: actions/upload-artifact@v7
         if: ${{ failure() }}
         name: Upload BanyanDB Data Folder
         with:
diff --git a/.github/workflows/test-integration-distributed.yml 
b/.github/workflows/test-integration-distributed.yml
index 66d0d9b23..40f1e4f1f 100644
--- a/.github/workflows/test-integration-distributed.yml
+++ b/.github/workflows/test-integration-distributed.yml
@@ -23,7 +23,7 @@ jobs:
   test-integration-distributed:
     strategy:
       matrix:
-        tz: ["UTC", "Asia/Shanghai", "America/Los_Angeles"]
+        tz: ["UTC", "America/Los_Angeles"]
     uses: ./.github/workflows/test.yml
     with:
       test-name: Integration Distributed
diff --git a/.github/workflows/test-integration-standalone.yml 
b/.github/workflows/test-integration-standalone.yml
index b54260ff6..3ec40b2ca 100644
--- a/.github/workflows/test-integration-standalone.yml
+++ b/.github/workflows/test-integration-standalone.yml
@@ -23,7 +23,7 @@ jobs:
   test-integration-standalone:
     strategy:
       matrix:
-        tz: ["UTC", "Asia/Shanghai", "America/Los_Angeles"]
+        tz: ["UTC", "America/Los_Angeles"]
     uses: ./.github/workflows/test.yml
     with:
       test-name: Integration Standalone
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 047328f69..306bd4572 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -75,7 +75,7 @@ jobs:
         if: ${{ failure() }}
         id: sanitize-name
         run: echo "sanitized=$(echo '${{ inputs.test-name }}-${{ 
inputs.timezone }}' | sed 's/[^a-zA-Z0-9._-]/-/g')" >> $GITHUB_OUTPUT
-      - uses: actions/upload-artifact@v4
+      - uses: actions/upload-artifact@v7
         if: ${{ failure() }}
         name: Upload BanyanDB Data Folder
         with:
diff --git a/CHANGES.md b/CHANGES.md
index 48c7daebc..2b5010d24 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -38,6 +38,7 @@ Release Notes.
 - Fix the lifecycle panic when the trace has no sidx.
 - Fix panic in sidx merge and flush operations when part counts don't match 
expectations.
 - Fix trace queries with range conditions on the same tag (e.g., duration) 
combined with ORDER BY by deduplicating tag names when merging logical 
expression branches.
+- Fix sidx tag filter range check returning inverted skip decision and use 
correct int64 encoding for block min/max.
 
 ### Document
 
diff --git a/banyand/cmd/dump/sidx.go b/banyand/cmd/dump/sidx.go
index e4425a063..c084119b1 100644
--- a/banyand/cmd/dump/sidx.go
+++ b/banyand/cmd/dump/sidx.go
@@ -39,6 +39,7 @@ import (
        "github.com/apache/skywalking-banyandb/banyand/internal/sidx"
        "github.com/apache/skywalking-banyandb/banyand/protector"
        "github.com/apache/skywalking-banyandb/pkg/convert"
+       "github.com/apache/skywalking-banyandb/pkg/encoding"
        "github.com/apache/skywalking-banyandb/pkg/fs"
        "github.com/apache/skywalking-banyandb/pkg/index/inverted"
        "github.com/apache/skywalking-banyandb/pkg/logger"
@@ -797,6 +798,13 @@ func dumpSidxFullScan(sidxPath, segmentPath string, 
criteria *modelv1.Criteria,
                }
        }
 
+       // Discover projected tag value types so binary values (for example 
int64) can be rendered as readable values.
+       projectionTagTypes, err := discoverProjectionTagTypes(sidxPath, 
projectionTagNames)
+       if err != nil {
+               fmt.Fprintf(os.Stderr, "Warning: failed to discover projection 
tag types: %v\n", err)
+               projectionTagTypes = nil
+       }
+
        // Create dynamic tag registry if criteria is provided
        var tagRegistry *dynamicTagRegistry
        if criteria != nil {
@@ -878,9 +886,9 @@ func dumpSidxFullScan(sidxPath, segmentPath string, 
criteria *modelv1.Criteria,
 
        // Output results
        if csvOutput {
-               return outputScanResultsAsCSV(results, seriesMap, 
projectionTagNames, dataFilter)
+               return outputScanResultsAsCSV(results, seriesMap, 
projectionTagNames, projectionTagTypes, dataFilter)
        }
-       return outputScanResultsAsText(results, sidxPath, seriesMap, 
projectionTagNames, dataFilter)
+       return outputScanResultsAsText(results, sidxPath, seriesMap, 
projectionTagNames, projectionTagTypes, dataFilter)
 }
 
 // parseProjectionTags parses a comma-separated list of tag names.
@@ -939,7 +947,9 @@ func loadSeriesMap(segmentPath string) 
(map[common.SeriesID]string, error) {
        return seriesMap, nil
 }
 
-func outputScanResultsAsText(results []*sidx.QueryResponse, sidxPath string, 
seriesMap map[common.SeriesID]string, projectionTagNames []string, dataFilter 
string) error {
+func outputScanResultsAsText(results []*sidx.QueryResponse, sidxPath string, 
seriesMap map[common.SeriesID]string,
+       projectionTagNames []string, projectionTagTypes 
map[string]pbv1.ValueType, dataFilter string,
+) error {
        fmt.Printf("Opening sidx: %s\n", sidxPath)
        
fmt.Printf("================================================================================\n\n")
 
@@ -984,9 +994,9 @@ func outputScanResultsAsText(results []*sidx.QueryResponse, 
sidxPath string, ser
                        if len(projectionTagNames) > 0 && resp.Tags != nil {
                                for _, tagName := range projectionTagNames {
                                        if tagValues, ok := resp.Tags[tagName]; 
ok && i < len(tagValues) {
-                                               tagValue := tagValues[i]
+                                               tagValue := 
decodeProjectedTagValue(tagValues[i], projectionTagTypes[tagName])
                                                // Calculate size of the tag 
value
-                                               tagSize := len(tagValue)
+                                               tagSize := len(tagValues[i])
                                                fmt.Printf("  %s: %s (size: %d 
bytes)\n", tagName, tagValue, tagSize)
                                        }
                                }
@@ -1008,7 +1018,9 @@ func outputScanResultsAsText(results 
[]*sidx.QueryResponse, sidxPath string, ser
        return nil
 }
 
-func outputScanResultsAsCSV(results []*sidx.QueryResponse, seriesMap 
map[common.SeriesID]string, projectionTagNames []string, dataFilter string) 
error {
+func outputScanResultsAsCSV(results []*sidx.QueryResponse, seriesMap 
map[common.SeriesID]string,
+       projectionTagNames []string, projectionTagTypes 
map[string]pbv1.ValueType, dataFilter string,
+) error {
        writer := csv.NewWriter(os.Stdout)
        defer writer.Flush()
 
@@ -1058,9 +1070,9 @@ func outputScanResultsAsCSV(results 
[]*sidx.QueryResponse, seriesMap map[common.
 
                                if resp.Tags != nil {
                                        if tagValues, ok := resp.Tags[tagName]; 
ok && i < len(tagValues) {
-                                               tagValue = tagValues[i]
+                                               tagValue = 
decodeProjectedTagValue(tagValues[i], projectionTagTypes[tagName])
                                                // Calculate size of the tag 
value
-                                               tagSize = fmt.Sprintf("%d", 
len(tagValue))
+                                               tagSize = fmt.Sprintf("%d", 
len(tagValues[i]))
                                        }
                                }
 
@@ -1077,3 +1089,81 @@ func outputScanResultsAsCSV(results 
[]*sidx.QueryResponse, seriesMap map[common.
 
        return nil
 }
+
+func discoverProjectionTagTypes(sidxPath string, projectionTagNames []string) 
(map[string]pbv1.ValueType, error) {
+       if len(projectionTagNames) == 0 {
+               return nil, nil
+       }
+       partEntries, err := os.ReadDir(sidxPath)
+       if err != nil {
+               return nil, fmt.Errorf("failed to read sidx path %s: %w", 
sidxPath, err)
+       }
+       partPaths := make([]string, 0, len(partEntries))
+       for _, entry := range partEntries {
+               if !entry.IsDir() {
+                       continue
+               }
+               partPaths = append(partPaths, filepath.Join(sidxPath, 
entry.Name()))
+       }
+       sort.Strings(partPaths)
+
+       result := make(map[string]pbv1.ValueType, len(projectionTagNames))
+       for _, tagName := range projectionTagNames {
+               for _, partPath := range partPaths {
+                       tmPath := filepath.Join(partPath, tagName+".tm")
+                       data, readErr := os.ReadFile(tmPath)
+                       if readErr != nil {
+                               continue
+                       }
+                       valueType, parseErr := parseSidxTagValueType(data)
+                       if parseErr != nil {
+                               fmt.Fprintf(os.Stderr, "Warning: failed to 
parse %s: %v\n", tmPath, parseErr)
+                               break
+                       }
+                       result[tagName] = valueType
+                       break
+               }
+       }
+       return result, nil
+}
+
+func parseSidxTagValueType(data []byte) (pbv1.ValueType, error) {
+       src, _, err := encoding.DecodeBytes(data)
+       if err != nil {
+               return pbv1.ValueTypeUnknown, fmt.Errorf("cannot decode tag 
name: %w", err)
+       }
+       if len(src) < 1 {
+               return pbv1.ValueTypeUnknown, fmt.Errorf("invalid tag metadata: 
missing value type")
+       }
+       return pbv1.ValueType(src[0]), nil
+}
+
+func decodeProjectedTagValue(raw string, valueType pbv1.ValueType) string {
+       if raw == "" {
+               return raw
+       }
+       rawBytes := []byte(raw)
+
+       switch valueType {
+       case pbv1.ValueTypeInt64:
+               if len(rawBytes) != 8 {
+                       return raw
+               }
+               return strconv.FormatInt(convert.BytesToInt64(rawBytes), 10)
+       case pbv1.ValueTypeFloat64:
+               if len(rawBytes) != 8 {
+                       return raw
+               }
+               return strconv.FormatFloat(convert.BytesToFloat64(rawBytes), 
'f', -1, 64)
+       case pbv1.ValueTypeTimestamp:
+               if len(rawBytes) != 8 {
+                       return raw
+               }
+               nanos := convert.BytesToInt64(rawBytes)
+               return strconv.FormatInt(nanos, 10)
+       case pbv1.ValueTypeBinaryData:
+               return fmt.Sprintf("%x", rawBytes)
+       default:
+               return raw
+       }
+}
diff --git a/banyand/internal/sidx/block.go b/banyand/internal/sidx/block.go
index 4c68bad3e..c240dbbfc 100644
--- a/banyand/internal/sidx/block.go
+++ b/banyand/internal/sidx/block.go
@@ -313,7 +313,7 @@ func (b *block) mustWriteTag(tagName string, td *tagData, 
bm *blockMetadata, ww
                if len(v) != 8 {
                        return
                }
-               val := encoding.BytesToInt64(v)
+               val := convert.BytesToInt64(v)
                if !hasMinMax {
                        minVal = val
                        maxVal = val
@@ -351,8 +351,8 @@ func (b *block) mustWriteTag(tagName string, td *tagData, 
bm *blockMetadata, ww
        }
 
        if td.valueType == pbv1.ValueTypeInt64 && hasMinMax {
-               tm.min = encoding.Int64ToBytes(nil, minVal)
-               tm.max = encoding.Int64ToBytes(nil, maxVal)
+               tm.min = convert.Int64ToBytes(minVal)
+               tm.max = convert.Int64ToBytes(maxVal)
        }
        isDictionaryEncoded := encodeType == encoding.EncodeTypeDictionary
        if !isDictionaryEncoded {
@@ -543,7 +543,7 @@ func fullTagAppend(bi, b *blockPointer, offset int) {
        }
 }
 
-func assertIdxAndOffset(name string, length int, idx int, offset int) {
+func assertIdxAndOffset(name string, length, idx, offset int) {
        if idx >= offset {
                logger.Panicf("%q idx %d must be less than offset %d", name, 
idx, offset)
        }
diff --git a/banyand/internal/sidx/block_scanner.go 
b/banyand/internal/sidx/block_scanner.go
index a7b39daa7..bfa1f7b3c 100644
--- a/banyand/internal/sidx/block_scanner.go
+++ b/banyand/internal/sidx/block_scanner.go
@@ -20,6 +20,7 @@ package sidx
 import (
        "context"
        "fmt"
+       "sort"
 
        "github.com/dustin/go-humanize"
 
@@ -28,9 +29,14 @@ import (
        "github.com/apache/skywalking-banyandb/pkg/index"
        "github.com/apache/skywalking-banyandb/pkg/logger"
        "github.com/apache/skywalking-banyandb/pkg/pool"
+       "github.com/apache/skywalking-banyandb/pkg/query"
 )
 
-const blockScannerBatchSize = 32
+const (
+       blockScannerBatchSize  = 32
+       perBlockSkipTraceLimit = 20
+       skipSummaryTopN        = 3
+)
 
 type blockScanResult struct {
        p  *part
@@ -72,19 +78,36 @@ func releaseBlockScanResultBatch(bsb *blockScanResultBatch) 
{
 
 var blockScanResultBatchPool = 
pool.Register[*blockScanResultBatch]("sidx-blockScannerBatch")
 
+type blockSkipRecorderFunc func(reason string, bm *blockMetadata)
+
+type blockSkipDetail struct {
+       reason   string
+       seriesID common.SeriesID
+       minKey   int64
+       maxKey   int64
+}
+
+type blockSkipStats struct {
+       reasonCounts map[string]int
+       details      []blockSkipDetail
+       totalSkipped int
+}
+
 type scanFinalizer func()
 
 type blockScanner struct {
        pm         protector.Memory
        filter     index.Filter
        l          *logger.Logger
+       span       *query.Span
+       skipStats  *blockSkipStats
        parts      []*part
        finalizers []scanFinalizer
        seriesIDs  []common.SeriesID
        minKey     int64
        maxKey     int64
-       asc        bool
        batchSize  int
+       asc        bool
 }
 
 func (bsn *blockScanner) scan(ctx context.Context, blockCh chan 
*blockScanResultBatch) {
@@ -96,10 +119,37 @@ func (bsn *blockScanner) scan(ctx context.Context, blockCh 
chan *blockScanResult
                return
        }
 
+       var (
+               totalBlockBytes uint64
+               scannedBlocks   int
+       )
+
+       var skipRecorder blockSkipRecorderFunc
+       if tracer := query.GetTracer(ctx); tracer != nil {
+               span, spanCtx := tracer.StartSpan(ctx, "sidx.scan-blocks")
+               bsn.span = span
+               ctx = spanCtx
+               skipRecorder = bsn.recordSkip
+               span.Tagf("part_count", "%d", len(bsn.parts))
+               span.Tagf("series_id_count", "%d", len(bsn.seriesIDs))
+               span.Tagf("min_key", "%d", bsn.minKey)
+               span.Tagf("max_key", "%d", bsn.maxKey)
+               span.Tagf("ascending", "%t", bsn.asc)
+               span.Tagf("batch_size", "%d", bsn.batchSize)
+               defer func() {
+                       if span != nil {
+                               span.Tagf("scanned_blocks", "%d", scannedBlocks)
+                               span.Tagf("total_block_bytes", "%d", 
totalBlockBytes)
+                               bsn.flushSkipSummaryToSpan()
+                               span.Stop()
+                       }
+               }()
+       }
+
        it := generateIter()
        defer releaseIter(it)
 
-       it.init(bsn.parts, bsn.seriesIDs, bsn.minKey, bsn.maxKey, bsn.filter, 
bsn.asc)
+       it.init(bsn.parts, bsn.seriesIDs, bsn.minKey, bsn.maxKey, bsn.filter, 
bsn.asc, skipRecorder)
 
        batch := generateBlockScanResultBatch()
        if it.Error() != nil {
@@ -113,7 +163,6 @@ func (bsn *blockScanner) scan(ctx context.Context, blockCh 
chan *blockScanResult
                batchThreshold = blockScannerBatchSize
        }
 
-       var totalBlockBytes uint64
        for it.nextBlock() {
                if !bsn.checkContext(ctx) {
                        releaseBlockScanResultBatch(batch)
@@ -141,6 +190,7 @@ func (bsn *blockScanner) scan(ctx context.Context, blockCh 
chan *blockScanResult
                // Quota OK, add block to batch
                bsn.addBlockToBatch(batch, bm, p)
                totalBlockBytes += blockSize
+               scannedBlocks++
 
                // Check if batch is full
                if len(batch.bss) >= batchThreshold || len(batch.bss) >= 
cap(batch.bss) {
@@ -234,3 +284,79 @@ func (bsn *blockScanner) close() {
                bsn.finalizers[i]()
        }
 }
+
+func (bsn *blockScanner) recordSkip(reason string, bm *blockMetadata) {
+       if bsn == nil || reason == "" {
+               return
+       }
+       if bsn.skipStats == nil {
+               bsn.skipStats = &blockSkipStats{
+                       reasonCounts: make(map[string]int),
+                       details:      make([]blockSkipDetail, 0, 
perBlockSkipTraceLimit),
+               }
+       }
+       bsn.skipStats.totalSkipped++
+       bsn.skipStats.reasonCounts[reason]++
+       if len(bsn.skipStats.details) >= perBlockSkipTraceLimit || bm == nil {
+               return
+       }
+       bsn.skipStats.details = append(bsn.skipStats.details, blockSkipDetail{
+               reason:   reason,
+               seriesID: bm.seriesID,
+               minKey:   bm.minKey,
+               maxKey:   bm.maxKey,
+       })
+}
+
+func (bsn *blockScanner) flushSkipSummaryToSpan() {
+       if bsn == nil || bsn.span == nil || bsn.skipStats == nil || 
bsn.skipStats.totalSkipped == 0 {
+               return
+       }
+
+       stats := bsn.skipStats
+       bsn.span.Tagf("skipped_total", "%d", stats.totalSkipped)
+
+       if len(stats.reasonCounts) == 0 {
+               return
+       }
+
+       type reasonCount struct {
+               reason string
+               count  int
+       }
+
+       reasons := make([]reasonCount, 0, len(stats.reasonCounts))
+       for reason, count := range stats.reasonCounts {
+               reasons = append(reasons, reasonCount{
+                       reason: reason,
+                       count:  count,
+               })
+       }
+
+       sort.Slice(reasons, func(i, j int) bool {
+               return reasons[i].count > reasons[j].count
+       })
+
+       otherCount := 0
+       for i, rc := range reasons {
+               if i < skipSummaryTopN {
+                       idx := i + 1
+                       bsn.span.Tag(fmt.Sprintf("skipped_reason_%d", idx), 
rc.reason)
+                       bsn.span.Tagf(fmt.Sprintf("skipped_reason_%d_count", 
idx), "%d", rc.count)
+                       continue
+               }
+               otherCount += rc.count
+       }
+
+       if otherCount > 0 && len(reasons) > skipSummaryTopN {
+               bsn.span.Tagf("skipped_other_reason_count", "%d", otherCount)
+       }
+
+       for i, detail := range stats.details {
+               prefix := fmt.Sprintf("skip_%d_", i)
+               bsn.span.Tag(prefix+"reason", detail.reason)
+               bsn.span.Tagf(prefix+"series_id", "%d", detail.seriesID)
+               bsn.span.Tagf(prefix+"min_key", "%d", detail.minKey)
+               bsn.span.Tagf(prefix+"max_key", "%d", detail.maxKey)
+       }
+}
diff --git a/banyand/internal/sidx/block_scanner_test.go 
b/banyand/internal/sidx/block_scanner_test.go
index 1e9a4fca1..b30f80bc1 100644
--- a/banyand/internal/sidx/block_scanner_test.go
+++ b/banyand/internal/sidx/block_scanner_test.go
@@ -20,6 +20,7 @@ package sidx
 import (
        "context"
        "errors"
+       "strconv"
        "testing"
 
        "github.com/stretchr/testify/assert"
@@ -30,6 +31,7 @@ import (
        "github.com/apache/skywalking-banyandb/pkg/index"
        "github.com/apache/skywalking-banyandb/pkg/logger"
        pbv1 "github.com/apache/skywalking-banyandb/pkg/pb/v1"
+       "github.com/apache/skywalking-banyandb/pkg/query"
 )
 
 func TestBlockScannerStructures(t *testing.T) {
@@ -453,3 +455,98 @@ func TestBlockScanner_BatchSizeHandling(t *testing.T) {
 
        scanner.close()
 }
+
+func TestBlockScanner_SpanRecordsSkippedBlocks(t *testing.T) {
+       // Build a simple in-memory part with data so that iter produces at 
least one block,
+       // then apply a block filter that skips all blocks and verify the 
scan-blocks span
+       // records the skip reason and summary tags.
+       testElements := []testElement{
+               {
+                       seriesID: common.SeriesID(1),
+                       userKey:  100,
+                       data:     []byte("data1"),
+               },
+               {
+                       seriesID: common.SeriesID(1),
+                       userKey:  200,
+                       data:     []byte("data2"),
+               },
+       }
+
+       elements := createTestElements(testElements)
+       defer releaseElements(elements)
+
+       mp := GenerateMemPart()
+       defer ReleaseMemPart(mp)
+       mp.mustInitFromElements(elements)
+
+       testPart := openMemPart(mp)
+       defer testPart.close()
+
+       // Block filter that skips every candidate block.
+       skipAllFilter := &mockBlockFilter{shouldSkip: true}
+
+       scanner := &blockScanner{
+               pm:        &test.MockMemoryProtector{ExpectQuotaExceeded: 
false},
+               l:         logger.GetLogger(),
+               parts:     []*part{testPart},
+               seriesIDs: []common.SeriesID{1},
+               minKey:    0,
+               maxKey:    1000,
+               filter:    skipAllFilter,
+               asc:       true,
+       }
+
+       // Attach a tracer to the context so blockScanner.scan creates the 
sidx.scan-blocks span.
+       baseCtx := context.Background()
+       tracer, tracedCtx := query.NewTracer(baseCtx, "test-trace-skip-blocks")
+       require.NotNil(t, tracer)
+
+       blockCh := make(chan *blockScanResultBatch, 4)
+
+       go func() {
+               defer close(blockCh)
+               scanner.scan(tracedCtx, blockCh)
+       }()
+
+       for batch := range blockCh {
+               if batch != nil {
+                       releaseBlockScanResultBatch(batch)
+               }
+       }
+
+       scanner.close()
+
+       trace := tracer.ToProto()
+       require.NotNil(t, trace)
+
+       var scanSpanTags map[string]string
+       for _, span := range trace.Spans {
+               if span.GetMessage() != "sidx.scan-blocks" {
+                       continue
+               }
+               scanSpanTags = make(map[string]string, len(span.Tags))
+               for _, tag := range span.Tags {
+                       if tag == nil {
+                               continue
+                       }
+                       scanSpanTags[tag.Key] = tag.Value
+               }
+               break
+       }
+
+       require.NotNil(t, scanSpanTags, "expected sidx.scan-blocks span to be 
present")
+
+       // Verify that total skipped blocks is recorded.
+       skippedTotal, ok := scanSpanTags["skipped_total"]
+       require.True(t, ok, "skipped_total tag must be present on scan-blocks 
span")
+       parsedTotal, err := strconv.Atoi(skippedTotal)
+       require.NoError(t, err, "skipped_total must be an integer")
+       require.Greater(t, parsedTotal, 0, "skipped_total should be greater 
than 0")
+
+       // Verify that the dominant skip reason is captured and summarized.
+       require.Equal(t, "block_filter_should_skip", 
scanSpanTags["skipped_reason_1"])
+
+       // Verify that at least the first skipped block's detail is recorded.
+       require.Equal(t, "block_filter_should_skip", 
scanSpanTags["skip_0_reason"])
+}
diff --git a/banyand/internal/sidx/interfaces.go 
b/banyand/internal/sidx/interfaces.go
index 5c4a88219..d0c3e5d50 100644
--- a/banyand/internal/sidx/interfaces.go
+++ b/banyand/internal/sidx/interfaces.go
@@ -123,13 +123,11 @@ type ScanProgressFunc func(currentPart, totalParts int, 
rowsFound int)
 //nolint:govet // struct layout optimized for readability; 64 bytes is 
acceptable
 type ScanQueryRequest struct {
        TagFilter     model.TagFilterMatcher
-       TagProjection []model.TagProjection
        OnProgress    ScanProgressFunc
        MinKey        *int64
        MaxKey        *int64
+       TagProjection []model.TagProjection
        MaxBatchSize  int
-       // OnProgress is an optional callback for progress reporting during 
scan.
-       // Called after processing each part with the current progress.
 }
 
 // QueryResponse contains a batch of query results and execution metadata.
diff --git a/banyand/internal/sidx/iter.go b/banyand/internal/sidx/iter.go
index e48bd4a60..8e5665fb3 100644
--- a/banyand/internal/sidx/iter.go
+++ b/banyand/internal/sidx/iter.go
@@ -71,7 +71,7 @@ func (it *iter) reset() {
        it.asc = false
 }
 
-func (it *iter) init(parts []*part, sids []common.SeriesID, minKey, maxKey 
int64, blockFilter index.Filter, asc bool) {
+func (it *iter) init(parts []*part, sids []common.SeriesID, minKey, maxKey 
int64, blockFilter index.Filter, asc bool, skipRecorder blockSkipRecorderFunc) {
        it.reset()
        it.parts = append(it.parts[:0], parts...)
        it.asc = asc
@@ -94,7 +94,7 @@ func (it *iter) init(parts []*part, sids []common.SeriesID, 
minKey, maxKey int64
                pki := generatePartKeyIter()
                it.partIters[i] = pki
 
-               pki.init(p, sids, minKey, maxKey, blockFilter, asc)
+               pki.init(p, sids, minKey, maxKey, blockFilter, asc, 
skipRecorder)
                if err := pki.error(); err != nil {
                        if !errors.Is(err, io.EOF) {
                                releasePartKeyIter(pki)
diff --git a/banyand/internal/sidx/iter_test.go 
b/banyand/internal/sidx/iter_test.go
index fe4388ff3..b170549dd 100644
--- a/banyand/internal/sidx/iter_test.go
+++ b/banyand/internal/sidx/iter_test.go
@@ -270,7 +270,7 @@ func TestIterEdgeCases(t *testing.T) {
        t.Run("empty_parts_list", func(t *testing.T) {
                for _, asc := range []bool{true, false} {
                        it := generateIter()
-                       it.init(nil, []common.SeriesID{1, 2, 3}, 100, 200, nil, 
asc)
+                       it.init(nil, []common.SeriesID{1, 2, 3}, 100, 200, nil, 
asc, nil)
                        assert.False(t, it.nextBlock())
                        assert.Nil(t, it.Error())
                        releaseIter(it)
@@ -295,7 +295,7 @@ func TestIterEdgeCases(t *testing.T) {
 
                for _, asc := range []bool{true, false} {
                        it := generateIter()
-                       it.init([]*part{testPart}, []common.SeriesID{}, 0, 
1000, nil, asc)
+                       it.init([]*part{testPart}, []common.SeriesID{}, 0, 
1000, nil, asc, nil)
                        assert.False(t, it.nextBlock())
                        assert.Nil(t, it.Error())
                        releaseIter(it)
@@ -335,7 +335,7 @@ func TestIterEdgeCases(t *testing.T) {
                for _, asc := range []bool{true, false} {
                        it := generateIter()
                        // Query range that doesn't overlap with any blocks
-                       it.init([]*part{testPart1, testPart2}, 
[]common.SeriesID{1, 2}, 400, 500, nil, asc)
+                       it.init([]*part{testPart1, testPart2}, 
[]common.SeriesID{1, 2}, 400, 500, nil, asc, nil)
                        assert.False(t, it.nextBlock())
                        assert.Nil(t, it.Error())
                        releaseIter(it)
@@ -359,7 +359,7 @@ func TestIterEdgeCases(t *testing.T) {
 
                for _, asc := range []bool{true, false} {
                        it := generateIter()
-                       it.init([]*part{testPart}, []common.SeriesID{1}, 50, 
150, nil, asc)
+                       it.init([]*part{testPart}, []common.SeriesID{1}, 50, 
150, nil, asc, nil)
 
                        assert.True(t, it.nextBlock())
                        assert.False(t, it.nextBlock()) // Should be only one 
block
@@ -386,7 +386,7 @@ func TestIterEdgeCases(t *testing.T) {
 
                for _, asc := range []bool{true, false} {
                        it := generateIter()
-                       it.init([]*part{testPart}, []common.SeriesID{1}, 0, 
200, mockFilter, asc)
+                       it.init([]*part{testPart}, []common.SeriesID{1}, 0, 
200, mockFilter, asc, nil)
 
                        assert.False(t, it.nextBlock())
                        assert.Error(t, it.Error())
@@ -558,7 +558,7 @@ func TestIterOverlappingBlockGroups(t *testing.T) {
 
        for _, asc := range []bool{true, false} {
                it := generateIter()
-               it.init([]*part{part1, part2}, []common.SeriesID{1, 2}, 0, 500, 
nil, asc)
+               it.init([]*part{part1, part2}, []common.SeriesID{1, 2}, 0, 500, 
nil, asc, nil)
 
                // Now we iterate individual blocks from both parts
                partsSeen := make(map[*part]struct{})
@@ -603,7 +603,7 @@ func runIteratorPass(t *testing.T, tc iterTestCase, parts 
[]*part, asc bool) []b
        it := generateIter()
        defer releaseIter(it)
 
-       it.init(parts, tc.querySids, tc.minKey, tc.maxKey, tc.blockFilter, asc)
+       it.init(parts, tc.querySids, tc.minKey, tc.maxKey, tc.blockFilter, asc, 
nil)
 
        var foundBlocks []blockExpectation
 
diff --git a/banyand/internal/sidx/part_key_iter.go 
b/banyand/internal/sidx/part_key_iter.go
index edb7de9dc..8a894df24 100644
--- a/banyand/internal/sidx/part_key_iter.go
+++ b/banyand/internal/sidx/part_key_iter.go
@@ -130,7 +130,16 @@ func (sc *seriesCursor) advance() (bool, error) {
                        return false, fmt.Errorf("block index %d out of range 
for primary %d", ref.blockIdx, ref.primaryIdx)
                }
                bm := &bma.arr[ref.blockIdx]
-               if bm.maxKey < sc.iter.minKey || bm.minKey > sc.iter.maxKey {
+               if bm.maxKey < sc.iter.minKey {
+                       if sc.iter != nil {
+                               
sc.iter.recordBlockSkip("block_out_of_key_range_before_min_key", bm)
+                       }
+                       continue
+               }
+               if bm.minKey > sc.iter.maxKey {
+                       if sc.iter != nil {
+                               
sc.iter.recordBlockSkip("block_out_of_key_range_after_max_key", bm)
+                       }
                        continue
                }
                if sc.iter.blockFilter != nil {
@@ -139,6 +148,9 @@ func (sc *seriesCursor) advance() (bool, error) {
                                return false, err
                        }
                        if shouldSkip {
+                               if sc.iter != nil {
+                                       
sc.iter.recordBlockSkip("block_filter_should_skip", bm)
+                               }
                                continue
                        }
                }
@@ -184,17 +196,18 @@ func (sch *seriesCursorHeap) Pop() any {
 }
 
 type partKeyIter struct {
-       err                  error
        blockFilter          index.Filter
+       err                  error
+       recordSkip           blockSkipRecorderFunc
        sidSet               map[common.SeriesID]struct{}
        p                    *part
        primaryCache         map[int]*blockMetadataArray
        curBlock             *blockMetadata
        cursorPool           []seriesCursor
-       cursorHeap           seriesCursorHeap
        sids                 []common.SeriesID
        primaryBuf           []byte
        compressedPrimaryBuf []byte
+       cursorHeap           seriesCursorHeap
        minKey               int64
        maxKey               int64
        asc                  bool
@@ -243,15 +256,17 @@ func (pki *partKeyIter) reset() {
 
        pki.compressedPrimaryBuf = pki.compressedPrimaryBuf[:0]
        pki.primaryBuf = pki.primaryBuf[:0]
+       pki.recordSkip = nil
 }
 
-func (pki *partKeyIter) init(p *part, sids []common.SeriesID, minKey, maxKey 
int64, blockFilter index.Filter, asc bool) {
+func (pki *partKeyIter) init(p *part, sids []common.SeriesID, minKey, maxKey 
int64, blockFilter index.Filter, asc bool, skipRecorder blockSkipRecorderFunc) {
        pki.reset()
        pki.p = p
        pki.minKey = minKey
        pki.maxKey = maxKey
        pki.blockFilter = blockFilter
        pki.asc = asc
+       pki.recordSkip = skipRecorder
 
        if len(sids) == 0 {
                pki.err = io.EOF
@@ -466,6 +481,13 @@ func (pki *partKeyIter) shouldSkipBlock(bm *blockMetadata) 
(bool, error) {
        return pki.blockFilter.ShouldSkip(tfo)
 }
 
+func (pki *partKeyIter) recordBlockSkip(reason string, bm *blockMetadata) {
+       if pki == nil || pki.recordSkip == nil {
+               return
+       }
+       pki.recordSkip(reason, bm)
+}
+
 func (pki *partKeyIter) current() (*blockMetadata, *part) {
        return pki.curBlock, pki.p
 }
diff --git a/banyand/internal/sidx/part_key_iter_test.go 
b/banyand/internal/sidx/part_key_iter_test.go
index f31d75007..67ce1bd08 100644
--- a/banyand/internal/sidx/part_key_iter_test.go
+++ b/banyand/internal/sidx/part_key_iter_test.go
@@ -58,7 +58,7 @@ func runPartKeyIterPass(t *testing.T, part *part, sids 
[]common.SeriesID, minKey
        iter := generatePartKeyIter()
        defer releasePartKeyIter(iter)
 
-       iter.init(part, sids, minKey, maxKey, blockFilter, asc)
+       iter.init(part, sids, minKey, maxKey, blockFilter, asc, nil)
 
        var results []blockExpectation
        for iter.nextBlock() {
@@ -261,7 +261,7 @@ func TestPartKeyIterGroupsOverlappingRanges(t *testing.T) {
 
        iter := generatePartKeyIter()
        defer releasePartKeyIter(iter)
-       iter.init(part, []common.SeriesID{1, 2, 3}, 0, 500, nil, true)
+       iter.init(part, []common.SeriesID{1, 2, 3}, 0, 500, nil, true, nil)
        var ids []common.SeriesID
        for iter.nextBlock() {
                block, _ := iter.current()
@@ -372,7 +372,7 @@ func TestPartKeyIterExhaustion(t *testing.T) {
                        iter := generatePartKeyIter()
                        defer releasePartKeyIter(iter)
 
-                       iter.init(part, []common.SeriesID{1, 2}, 0, 200, nil, 
asc)
+                       iter.init(part, []common.SeriesID{1, 2}, 0, 200, nil, 
asc, nil)
 
                        blockCount := 0
                        for iter.nextBlock() {
@@ -450,7 +450,7 @@ func TestPartKeyIterRequeuesOnGapBetweenBlocks(t 
*testing.T) {
                        iter := generatePartKeyIter()
                        defer releasePartKeyIter(iter)
 
-                       iter.init(part, []common.SeriesID{1}, 0, 100000, nil, 
asc)
+                       iter.init(part, []common.SeriesID{1}, 0, 100000, nil, 
asc, nil)
 
                        var blocks []struct {
                                min int64
diff --git a/banyand/internal/sidx/tag_filter_op.go 
b/banyand/internal/sidx/tag_filter_op.go
index 638624143..301e6923d 100644
--- a/banyand/internal/sidx/tag_filter_op.go
+++ b/banyand/internal/sidx/tag_filter_op.go
@@ -21,6 +21,9 @@ import (
        "bytes"
        "fmt"
 
+       "github.com/blugelabs/bluge/numeric"
+
+       "github.com/apache/skywalking-banyandb/pkg/convert"
        "github.com/apache/skywalking-banyandb/pkg/encoding"
        "github.com/apache/skywalking-banyandb/pkg/filter"
        "github.com/apache/skywalking-banyandb/pkg/fs"
@@ -148,7 +151,7 @@ func (tfo *tagFilterOp) Range(tagName string, rangeOpts 
index.RangeOpts) (bool,
 
        // Only perform range check for numeric types with min/max values
        if cache.valueType != pbv1.ValueTypeInt64 || len(cache.min) == 0 || 
len(cache.max) == 0 {
-               return true, nil // Conservative approach for non-numeric or 
missing min/max
+               return false, nil // Conservative approach for non-numeric or 
missing min/max - don't skip
        }
 
        // Check lower bound
@@ -157,10 +160,9 @@ func (tfo *tagFilterOp) Range(tagName string, rangeOpts 
index.RangeOpts) (bool,
                if !ok {
                        return false, fmt.Errorf("lower bound is not a float 
value: %v", rangeOpts.Lower)
                }
-               value := make([]byte, 0)
-               value = encoding.Int64ToBytes(value, int64(lower.Value))
+               value := 
convert.Int64ToBytes(numeric.Float64ToInt64(lower.Value))
                if bytes.Compare(cache.max, value) == -1 || 
(!rangeOpts.IncludesLower && bytes.Equal(cache.max, value)) {
-                       return false, nil
+                       return true, nil
                }
        }
 
@@ -170,14 +172,13 @@ func (tfo *tagFilterOp) Range(tagName string, rangeOpts 
index.RangeOpts) (bool,
                if !ok {
                        return false, fmt.Errorf("upper bound is not a float 
value: %v", rangeOpts.Upper)
                }
-               value := make([]byte, 0)
-               value = encoding.Int64ToBytes(value, int64(upper.Value))
+               value := 
convert.Int64ToBytes(numeric.Float64ToInt64(upper.Value))
                if bytes.Compare(cache.min, value) == 1 || 
(!rangeOpts.IncludesUpper && bytes.Equal(cache.min, value)) {
-                       return false, nil
+                       return true, nil
                }
        }
 
-       return true, nil
+       return false, nil
 }
 
 // getTagFilterCache retrieves or creates cached tag filter data.
diff --git a/banyand/internal/sidx/tag_filter_op_test.go 
b/banyand/internal/sidx/tag_filter_op_test.go
index 8357e9fc6..4a3e55213 100644
--- a/banyand/internal/sidx/tag_filter_op_test.go
+++ b/banyand/internal/sidx/tag_filter_op_test.go
@@ -22,9 +22,11 @@ import (
        "fmt"
        "testing"
 
+       "github.com/blugelabs/bluge/numeric"
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
 
+       "github.com/apache/skywalking-banyandb/pkg/convert"
        "github.com/apache/skywalking-banyandb/pkg/encoding"
        "github.com/apache/skywalking-banyandb/pkg/filter"
        "github.com/apache/skywalking-banyandb/pkg/index"
@@ -225,11 +227,11 @@ func TestTagFilterOpRange(t *testing.T) {
 }
 
 func TestTagFilterOpRangeWithCache(t *testing.T) {
-       // Create a cache with numeric data
+       // Create a cache with numeric data using convert.Int64ToBytes 
(order-preserving encoding)
        cache := &tagFilterCache{
                valueType: pbv1.ValueTypeInt64,
-               min:       encoding.Int64ToBytes(nil, 100),
-               max:       encoding.Int64ToBytes(nil, 500),
+               min:       convert.Int64ToBytes(100),
+               max:       convert.Int64ToBytes(500),
        }
 
        tfo := &tagFilterOp{
@@ -252,72 +254,73 @@ func TestTagFilterOpRangeWithCache(t *testing.T) {
                expectError    bool
        }{
                {
-                       name: "range completely below",
-                       rangeOpts: index.RangeOpts{
-                               Lower:         &index.FloatTermValue{Value: 10},
-                               Upper:         &index.FloatTermValue{Value: 50},
-                               IncludesLower: true,
-                               IncludesUpper: true,
-                       },
+                       name:           "range completely below",
+                       rangeOpts:      index.NewIntRangeOpts(10, 50, true, 
true),
+                       expectedResult: true,
+                       expectError:    false,
+                       description:    "should skip when range is completely 
below min",
+               },
+               {
+                       name:           "range completely above",
+                       rangeOpts:      index.NewIntRangeOpts(600, 800, true, 
true),
+                       expectedResult: true,
+                       expectError:    false,
+                       description:    "should skip when range is completely 
above max",
+               },
+               {
+                       name:           "range overlaps",
+                       rangeOpts:      index.NewIntRangeOpts(200, 300, true, 
true),
                        expectedResult: false,
                        expectError:    false,
-                       description:    "should return false when range is 
completely below min",
+                       description:    "should not skip when range overlaps 
with min/max",
                },
                {
-                       name: "range completely above",
-                       rangeOpts: index.RangeOpts{
-                               Lower:         &index.FloatTermValue{Value: 
600},
-                               Upper:         &index.FloatTermValue{Value: 
800},
-                               IncludesLower: true,
-                               IncludesUpper: true,
-                       },
+                       name:           "range contains all",
+                       rangeOpts:      index.NewIntRangeOpts(50, 600, true, 
true),
                        expectedResult: false,
                        expectError:    false,
-                       description:    "should return false when range is 
completely above max",
+                       description:    "should not skip when range contains 
all values",
                },
                {
-                       name: "range overlaps",
+                       name: "lower boundary exclusive miss",
                        rangeOpts: index.RangeOpts{
-                               Lower:         &index.FloatTermValue{Value: 
200},
-                               Upper:         &index.FloatTermValue{Value: 
300},
-                               IncludesLower: true,
-                               IncludesUpper: true,
+                               Lower:         &index.FloatTermValue{Value: 
numeric.Int64ToFloat64(500)},
+                               IncludesLower: false,
                        },
                        expectedResult: true,
                        expectError:    false,
-                       description:    "should return true when range overlaps 
with min/max",
+                       description:    "should skip when lower boundary is 
exclusive and equals max",
                },
                {
-                       name: "range contains all",
+                       name: "upper boundary exclusive miss",
                        rangeOpts: index.RangeOpts{
-                               Lower:         &index.FloatTermValue{Value: 50},
-                               Upper:         &index.FloatTermValue{Value: 
600},
-                               IncludesLower: true,
-                               IncludesUpper: true,
+                               Upper:         &index.FloatTermValue{Value: 
numeric.Int64ToFloat64(100)},
+                               IncludesUpper: false,
                        },
                        expectedResult: true,
                        expectError:    false,
-                       description:    "should return true when range contains 
all values",
+                       description:    "should skip when upper boundary is 
exclusive and equals min",
                },
                {
-                       name: "lower boundary exclusive miss",
-                       rangeOpts: index.RangeOpts{
-                               Lower:         &index.FloatTermValue{Value: 
500},
-                               IncludesLower: false,
-                       },
+                       name:           "inclusive lower equals block max",
+                       rangeOpts:      index.NewIntRangeOpts(500, 600, true, 
true),
                        expectedResult: false,
                        expectError:    false,
-                       description:    "should return false when lower 
boundary is exclusive and equals max",
+                       description:    "should not skip when lower is 
inclusive and equals max",
                },
                {
-                       name: "upper boundary exclusive miss",
-                       rangeOpts: index.RangeOpts{
-                               Upper:         &index.FloatTermValue{Value: 
100},
-                               IncludesUpper: false,
-                       },
+                       name:           "inclusive upper equals block min",
+                       rangeOpts:      index.NewIntRangeOpts(50, 100, true, 
true),
                        expectedResult: false,
                        expectError:    false,
-                       description:    "should return false when upper 
boundary is exclusive and equals min",
+                       description:    "should not skip when upper is 
inclusive and equals min",
+               },
+               {
+                       name:           "query above block max - adjacent 
exclusive",
+                       rangeOpts:      index.NewIntRangeOpts(200, 300, false, 
true),
+                       expectedResult: false,
+                       expectError:    false,
+                       description:    "should not skip when exclusive lower 
(200) is within block [100,500]",
                },
        }
 
@@ -356,16 +359,11 @@ func TestTagFilterOpRangeNonNumeric(t *testing.T) {
                },
        }
 
-       // For non-numeric types, should always return true (conservative 
approach)
-       result, err := tfo.Range("service", index.RangeOpts{
-               Lower:         &index.FloatTermValue{Value: 100},
-               Upper:         &index.FloatTermValue{Value: 200},
-               IncludesLower: true,
-               IncludesUpper: true,
-       })
+       // For non-numeric types, should not skip (conservative approach - 
return false)
+       result, err := tfo.Range("service", index.NewIntRangeOpts(100, 200, 
true, true))
 
        assert.NoError(t, err)
-       assert.True(t, result, "should return true for non-numeric types")
+       assert.False(t, result, "should not skip for non-numeric types 
(conservative approach)")
 }
 
 func TestDecodeBloomFilterFromBytes(t *testing.T) {
@@ -440,8 +438,8 @@ func BenchmarkTagFilterOpRange(b *testing.B) {
        // Setup
        cache := &tagFilterCache{
                valueType: pbv1.ValueTypeInt64,
-               min:       encoding.Int64ToBytes(nil, 100),
-               max:       encoding.Int64ToBytes(nil, 500),
+               min:       convert.Int64ToBytes(100),
+               max:       convert.Int64ToBytes(500),
        }
 
        tfo := &tagFilterOp{
@@ -456,12 +454,7 @@ func BenchmarkTagFilterOpRange(b *testing.B) {
                },
        }
 
-       rangeOpts := index.RangeOpts{
-               Lower:         &index.FloatTermValue{Value: 200},
-               Upper:         &index.FloatTermValue{Value: 300},
-               IncludesLower: true,
-               IncludesUpper: true,
-       }
+       rangeOpts := index.NewIntRangeOpts(200, 300, true, true)
 
        b.ResetTimer()
 
@@ -821,8 +814,8 @@ func TestTagFilterOpErrorHandling(t *testing.T) {
        t.Run("invalid range bounds", func(t *testing.T) {
                cache := &tagFilterCache{
                        valueType: pbv1.ValueTypeInt64,
-                       min:       encoding.Int64ToBytes(nil, 100),
-                       max:       encoding.Int64ToBytes(nil, 500),
+                       min:       convert.Int64ToBytes(100),
+                       max:       convert.Int64ToBytes(500),
                }
 
                tfo := &tagFilterOp{
diff --git 
a/test/cases/trace/data/input/duration_range_and_ipv4_order_timestamp.ql 
b/test/cases/trace/data/input/duration_range_and_ipv4_order_timestamp.ql
new file mode 100644
index 000000000..b5684c4f6
--- /dev/null
+++ b/test/cases/trace/data/input/duration_range_and_ipv4_order_timestamp.ql
@@ -0,0 +1,23 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (ASF) licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+SELECT () FROM TRACE zipkin IN zipkinTrace
+TIME > '-1h'
+WHERE duration >= 100 AND duration <= 1000 AND local_endpoint_ipv4 = 
'192.168.1.10'
+ORDER BY zipkin-timestamp DESC
+LIMIT 10
diff --git 
a/test/cases/trace/data/input/duration_range_and_ipv4_order_timestamp.yml 
b/test/cases/trace/data/input/duration_range_and_ipv4_order_timestamp.yml
new file mode 100644
index 000000000..19f0d8c7c
--- /dev/null
+++ b/test/cases/trace/data/input/duration_range_and_ipv4_order_timestamp.yml
@@ -0,0 +1,50 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (ASF) licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: "zipkin"
+groups: ["zipkinTrace"]
+limit: 10
+order_by:
+  index_rule_name: "zipkin-timestamp"
+  sort: "SORT_DESC"
+criteria:
+  le:
+    op: "LOGICAL_OP_AND"
+    left:
+      le:
+        op: "LOGICAL_OP_AND"
+        left:
+          condition:
+            name: "duration"
+            op: "BINARY_OP_GE"
+            value:
+              int:
+                value: 100
+        right:
+          condition:
+            name: "duration"
+            op: "BINARY_OP_LE"
+            value:
+              int:
+                value: 1000
+    right:
+      condition:
+        name: "local_endpoint_ipv4"
+        op: "BINARY_OP_EQ"
+        value:
+          str:
+            value: "192.168.1.10"
diff --git 
a/test/cases/trace/data/want/duration_range_and_ipv4_order_timestamp.yml 
b/test/cases/trace/data/want/duration_range_and_ipv4_order_timestamp.yml
new file mode 100644
index 000000000..40c9a153f
--- /dev/null
+++ b/test/cases/trace/data/want/duration_range_and_ipv4_order_timestamp.yml
@@ -0,0 +1,24 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (ASF) licenses this file to you under
+# the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+traces:
+  - spans:
+    - span: zipkin_trace_001_span_001
+      spanId: span_001
+    - span: zipkin_trace_001_span_002
+      spanId: span_002
+    traceId: zipkin_trace_001
diff --git a/test/cases/trace/trace.go b/test/cases/trace/trace.go
index cbe61d32c..c57ab8656 100644
--- a/test/cases/trace/trace.go
+++ b/test/cases/trace/trace.go
@@ -49,6 +49,8 @@ var _ = g.DescribeTable("Scanning Traces", func(args 
helpers.Args) {
        g.Entry("order by duration", helpers.Args{Input: "order_duration_desc", 
Duration: 1 * time.Hour}),
        g.Entry("duration range 10-1000 order by timestamp",
                helpers.Args{Input: "duration_range_order_timestamp", Duration: 
1 * time.Hour}),
+       g.Entry("duration range and ipv4 filter order by timestamp",
+               helpers.Args{Input: "duration_range_and_ipv4_order_timestamp", 
Duration: 1 * time.Hour}),
        g.Entry("filter by service id", helpers.Args{Input: 
"eq_service_order_timestamp_desc", Duration: 1 * time.Hour}),
        g.Entry("filter by service instance id", helpers.Args{Input: 
"eq_service_instance_order_time_asc", Duration: 1 * time.Hour}),
        g.Entry("filter by endpoint", helpers.Args{Input: 
"eq_endpoint_order_duration_asc", Duration: 1 * time.Hour}),


Reply via email to