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}),