This is an automated email from the ASF dual-hosted git repository. hanahmily pushed a commit to branch vectorized-query in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit fedf8cf43a9ba59088d71e75ed407b09f51e50dc Author: Hongtao Gao <[email protected]> AuthorDate: Fri May 15 10:04:56 2026 +0000 feat(query/vectorized/measure/plan): G9c boundary-error parity (nil TimeRange, projection, empty result) --- pkg/query/vectorized/measure/plan/dispatch.go | 107 +++++++++------ pkg/query/vectorized/measure/plan/dispatch_test.go | 152 +++++++++++++++------ test/cases/measure/data/input/empty_result.ql | 21 +++ test/cases/measure/data/input/empty_result.yaml | 32 +++++ .../data/input/project_non_existent_field.ql | 19 +++ .../data/input/project_non_existent_field.yaml | 25 ++++ test/cases/measure/measure.go | 2 + 7 files changed, 277 insertions(+), 81 deletions(-) diff --git a/pkg/query/vectorized/measure/plan/dispatch.go b/pkg/query/vectorized/measure/plan/dispatch.go index 3ada53ef1..baa870a2b 100644 --- a/pkg/query/vectorized/measure/plan/dispatch.go +++ b/pkg/query/vectorized/measure/plan/dispatch.go @@ -22,6 +22,8 @@ import ( "fmt" "sync/atomic" + "github.com/pkg/errors" + commonv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/common/v1" databasev1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/database/v1" measurev1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/measure/v1" @@ -146,9 +148,16 @@ func Dispatch( return nil, "", false, nil } } - if req.GetTimeRange() == nil { - return nil, "", false, nil - } + // G9c #9: a nil TimeRange is NOT a fall-through. The row path does not + // reject it — parseFields feeds criteria.GetTimeRange().GetBegin().AsTime() + // into the index scan, and a nil *timestamppb.Timestamp resolves to the + // Unix epoch (1970-01-01T00:00:00Z). The query then runs over the + // degenerate [epoch, epoch] window and yields an empty result. The + // scan.Params.TimeRange construction below mirrors that exactly + // (req.GetTimeRange().GetBegin().AsTime() == epoch when TimeRange is + // nil), so dispatch produces the row path's canonical empty response + // directly instead of borrowing it. + // Defensive nil guards on the runtime context. These should not fire // in production paths — buildMeasureContext populates all of them — // but a defensive fallthrough is safer than a nil dereference. @@ -156,13 +165,14 @@ func Dispatch( return nil, "", false, nil } - // Projection validation. The row path's Analyze rejects unknown - // projection names via ValidateProjectionTags / ValidateProjectionFields - // and surfaces a descriptive error. Dispatch falls through so the - // row path produces that canonical error (test fixtures with - // WantErr=true depend on it). - if !projectionsExistInSchema(req, measureSchema) { - return nil, "", false, nil + // G9c #11: projection validation. The row path's Analyze rejects + // unknown projection names via ValidateProjectionTags / + // ValidateProjectionFields and surfaces a descriptive error + // (test fixtures with WantErr=true assert it). Dispatch reproduces + // that canonical error byte-for-byte and returns handled=true so the + // caller surfaces it rather than falling through. + if projErr := validateProjectionParity(req, logicalSchema, measureSchema); projErr != nil { + return nil, "", true, projErr } // Hidden-tag detection: criteria may reference tags that are NOT in @@ -250,10 +260,13 @@ func Dispatch( return nil, "", true, fmt.Errorf("vec dispatch: query measure: %w", queryErr) } if result == nil { - // Match the row path's typed-nil handling: an empty query result - // flows through the row iterator as a no-op. Falling back lets - // that machinery surface the empty response unchanged. - return nil, "", false, nil + // G9c #13: a typed-nil result is the row path's canonical empty + // response. The row iterator (resultMIterator{result: nil}) reports + // Next()==false immediately and Close()==nil, so the client + // observes an empty []*measurev1.InternalDataPoint. Emit the same + // empty MIterator directly with handled=true instead of borrowing + // the row machinery. + return emptyMIterator{}, p.String(), true, nil } pool := vectorized.NewBatchPool(scan.BatchSchema, cfg.BatchSize) @@ -329,54 +342,64 @@ func projectedNames(tp *modelv1.TagProjection) map[string]struct{} { return out } -// projectionsExistInSchema returns false if any tag (in any requested tag -// family) or field name in the request's projection is absent from the -// Measure schema. Callers use the result as an eligibility gate: missing -// names route through the row path, which surfaces a descriptive error -// via logical_measure.Analyze. -func projectionsExistInSchema(req *measurev1.QueryRequest, m *databasev1.Measure) bool { +// validateProjectionParity reproduces, byte-for-byte, the projection +// errors the row path's logical_measure.Analyze raises so dispatch can +// surface the canonical WantErr=true message directly instead of falling +// through. It mirrors the row path exactly: +// +// - Tags are validated before fields (measure_analyzer.go:110-119). +// - Tag projection is checked only when non-empty; each projected tag +// (families in order, tags in order) is looked up schema-wide via the +// TagSpec registry — the logical.Schema equivalent of CommonSchema's +// TagSpecMap. The first miss returns errors.Wrap(ErrTagNotDefined, +// tagName), identical to CommonSchema.ValidateProjectionTags +// (schema.go:175): "<tagName>: tag is not defined". +// - Field projection is checked only when non-empty; the first name +// absent from the Measure schema's fields returns errors.Errorf( +// "field %s not found in schema", field), identical to +// measure.schema.ValidateProjectionFields (measure/schema.go:77). +// +// A nil error means every projected name resolves, so dispatch proceeds. +func validateProjectionParity(req *measurev1.QueryRequest, logicalSchema logical.Schema, m *databasev1.Measure) error { if tp := req.GetTagProjection(); tp != nil { for _, reqFamily := range tp.GetTagFamilies() { - schemaFamily := findSchemaTagFamily(m, reqFamily.GetName()) - if schemaFamily == nil { - return false - } - known := make(map[string]struct{}, len(schemaFamily.GetTags())) - for _, ts := range schemaFamily.GetTags() { - known[ts.GetName()] = struct{}{} - } for _, name := range reqFamily.GetTags() { - if _, ok := known[name]; !ok { - return false + if logicalSchema.FindTagSpecByName(name) == nil { + return errors.Wrap(logical.ErrTagNotDefined, name) } } } } if fp := req.GetFieldProjection(); fp != nil && len(fp.GetNames()) > 0 { + // The row path's m.fieldMap is built from md.GetFields() in + // logical_measure.BuildSchema, so the Measure schema's field set + // is the authoritative lookup the row path's + // ValidateProjectionFields consults. known := make(map[string]struct{}, len(m.GetFields())) for _, fs := range m.GetFields() { known[fs.GetName()] = struct{}{} } for _, name := range fp.GetNames() { if _, ok := known[name]; !ok { - return false + return errors.Errorf("field %s not found in schema", name) } } } - return true -} - -// findSchemaTagFamily returns the schema-defined tag family with the -// given name, or nil if no such family exists. -func findSchemaTagFamily(m *databasev1.Measure, name string) *databasev1.TagFamilySpec { - for _, tf := range m.GetTagFamilies() { - if tf.GetName() == name { - return tf - } - } return nil } +// emptyMIterator is the vec equivalent of the row path's +// resultMIterator{result: nil}: Next reports no rows, Current is never +// reached, and Close is a no-op error. Dispatch returns it for the +// canonical empty response (G9c #13). +type emptyMIterator struct{} + +func (emptyMIterator) Next() bool { return false } + +func (emptyMIterator) Current() []*measurev1.InternalDataPoint { return nil } + +func (emptyMIterator) Close() error { return nil } + // aggProjectionCoverage reports whether the request's GroupBy keys and // Agg field are all present in the request's projections. Required by // the dispatch eligibility gate: the BatchAggregation operator locates diff --git a/pkg/query/vectorized/measure/plan/dispatch_test.go b/pkg/query/vectorized/measure/plan/dispatch_test.go index d83a29191..08f9dcb6c 100644 --- a/pkg/query/vectorized/measure/plan/dispatch_test.go +++ b/pkg/query/vectorized/measure/plan/dispatch_test.go @@ -246,11 +246,12 @@ func TestDispatch_OrderBy_UnknownIndexRule_BubblesUpError(t *testing.T) { } } -// TestDispatch_UnknownTagProjection_FallsThrough covers the parity gap -// for WantErr=true fixtures: the row path rejects unknown tags via -// ValidateProjectionTags and returns a descriptive error. Dispatch -// falls through so the row path surfaces that canonical error. -func TestDispatch_UnknownTagProjection_FallsThrough(t *testing.T) { +// TestDispatch_UnknownTagProjection_SurfacesCanonicalError covers G9c +// #11: the row path rejects unknown tags via ValidateProjectionTags with +// errors.Wrap(ErrTagNotDefined, tagName). Dispatch reproduces that exact +// message and returns handled=true so the caller surfaces it (the +// WantErr=true fixtures depend on it) instead of borrowing the row path. +func TestDispatch_UnknownTagProjection_SurfacesCanonicalError(t *testing.T) { measureSchema := testMeasureSchema() // nolint:staticcheck // SA1019 — row-path BuildSchema is the only schema builder until G8 replaces it. logicalSchema, schemaErr := logicalmeasure.BuildSchema(measureSchema, nil) @@ -266,20 +267,27 @@ func TestDispatch_UnknownTagProjection_FallsThrough(t *testing.T) { }} _, _, handled, err := Dispatch(context.Background(), req, metadata, measureSchema, logicalSchema, ec, dispatchCfg(true)) - if err != nil { - t.Fatalf("unknown tag fallthrough must not error: %v", err) + if err == nil { + t.Fatal("unknown tag in projection must surface the canonical row-path error") } - if handled { - t.Fatal("unknown tag in projection must fall through (row path returns WantErr)") + // Byte-identical to logical.CommonSchema.ValidateProjectionTags + // (pkg/query/logical/schema.go:175): errors.Wrap(ErrTagNotDefined, "ghost"). + const wantMsg = "ghost: tag is not defined" + if err.Error() != wantMsg { + t.Fatalf("error message parity: want %q, got %q", wantMsg, err.Error()) + } + if !handled { + t.Fatal("projection error must report handled=true so caller surfaces it (no row-path retry)") } if ec.called { t.Fatal("ec.Query must not be invoked when projection is invalid") } } -// TestDispatch_UnknownFieldProjection_FallsThrough is the field-side -// counterpart of UnknownTagProjection. -func TestDispatch_UnknownFieldProjection_FallsThrough(t *testing.T) { +// TestDispatch_UnknownFieldProjection_SurfacesCanonicalError is the +// field-side counterpart: byte-identical to +// measure.schema.ValidateProjectionFields (measure/schema.go:77). +func TestDispatch_UnknownFieldProjection_SurfacesCanonicalError(t *testing.T) { measureSchema := testMeasureSchema() // nolint:staticcheck // SA1019 — row-path BuildSchema is the only schema builder until G8 replaces it. logicalSchema, schemaErr := logicalmeasure.BuildSchema(measureSchema, nil) @@ -293,29 +301,84 @@ func TestDispatch_UnknownFieldProjection_FallsThrough(t *testing.T) { req.FieldProjection = &measurev1.QueryRequest_FieldProjection{Names: []string{"ghost"}} _, _, handled, err := Dispatch(context.Background(), req, metadata, measureSchema, logicalSchema, ec, dispatchCfg(true)) - if err != nil { - t.Fatalf("unknown field fallthrough must not error: %v", err) + if err == nil { + t.Fatal("unknown field in projection must surface the canonical row-path error") } - if handled { - t.Fatal("unknown field in projection must fall through (row path returns WantErr)") + const wantMsg = "field ghost not found in schema" + if err.Error() != wantMsg { + t.Fatalf("error message parity: want %q, got %q", wantMsg, err.Error()) + } + if !handled { + t.Fatal("projection error must report handled=true so caller surfaces it (no row-path retry)") } if ec.called { t.Fatal("ec.Query must not be invoked when projection is invalid") } } -// TestDispatch_NoTimeRange_FallsThrough covers the bounded-window -// requirement. -func TestDispatch_NoTimeRange_FallsThrough(t *testing.T) { +// TestDispatch_TagValidatedBeforeField mirrors the row path's ordering +// (measure_analyzer.go validates tags before fields): when both +// projections are unknown, the tag error wins. +func TestDispatch_TagValidatedBeforeField(t *testing.T) { + measureSchema := testMeasureSchema() + // nolint:staticcheck // SA1019 — row-path BuildSchema is the only schema builder until G8 replaces it. + logicalSchema, schemaErr := logicalmeasure.BuildSchema(measureSchema, nil) + if schemaErr != nil { + t.Fatalf("BuildSchema: %v", schemaErr) + } + metadata := &commonv1.Metadata{Name: "demo", Group: "default"} + ec := &fakeEC{} + + req := bareReq() + req.TagProjection = &modelv1.TagProjection{TagFamilies: []*modelv1.TagProjection_TagFamily{ + {Name: "default", Tags: []string{"ghost"}}, + }} + req.FieldProjection = &measurev1.QueryRequest_FieldProjection{Names: []string{"phantom"}} + _, _, _, err := Dispatch(context.Background(), + req, metadata, measureSchema, logicalSchema, ec, dispatchCfg(true)) + if err == nil || err.Error() != "ghost: tag is not defined" { + t.Fatalf("tag error must take precedence over field error; got %v", err) + } +} + +// TestDispatch_NoTimeRange_EmptyResultParity covers G9c #9: a nil +// TimeRange is NOT rejected by the row path. parseFields feeds +// criteria.GetTimeRange().GetBegin().AsTime() (nil → Unix epoch) into the +// scan, the query runs over [epoch, epoch], and the client observes an +// empty response. Dispatch reproduces that exact behavior directly: +// ec.Query is invoked (over the epoch window) and the canonical empty +// MIterator is emitted with handled=true. +func TestDispatch_NoTimeRange_EmptyResultParity(t *testing.T) { + measureSchema := testMeasureSchema() + // nolint:staticcheck // SA1019 — row-path BuildSchema is the only schema builder until G8 replaces it. + logicalSchema, schemaErr := logicalmeasure.BuildSchema(measureSchema, nil) + if schemaErr != nil { + t.Fatalf("BuildSchema: %v", schemaErr) + } + metadata := &commonv1.Metadata{Name: "demo", Group: "default"} + ec := &fakeEC{wantResult: nil, wantErr: nil} + req := bareReq() req.TimeRange = nil - _, _, handled, err := Dispatch(context.Background(), - req, nil, nil, nil, nil, dispatchCfg(true)) + iter, _, handled, err := Dispatch(context.Background(), + req, metadata, measureSchema, logicalSchema, ec, dispatchCfg(true)) if err != nil { - t.Fatalf("no-TimeRange fallthrough must not error: %v", err) + t.Fatalf("nil TimeRange must not error (row path does not reject it): %v", err) } - if handled { - t.Fatal("missing TimeRange must fall through") + if !handled { + t.Fatal("nil TimeRange must be handled (row path produces an empty result, not a fall-through)") + } + if !ec.called { + t.Fatal("ec.Query must be invoked over the epoch window (row-path parity)") + } + if iter == nil { + t.Fatal("expected an empty MIterator, got nil") + } + if iter.Next() { + t.Fatal("empty-result MIterator must report Next()==false") + } + if closeErr := iter.Close(); closeErr != nil { + t.Fatalf("empty-result MIterator Close must be nil (row-path parity): %v", closeErr) } } @@ -348,13 +411,15 @@ func (f *fakeEC) Query(_ context.Context, opts model.MeasureQueryOptions) (model return f.wantResult, f.wantErr } -// TestDispatch_EmptyResult_FallsThrough exercises the full eligibility -// path: an eligible request reaches ec.Query, ec returns (nil, nil) -// (empty range), Dispatch reports fallthrough so the row path can surface -// the empty response. This also confirms the index.Query construction -// and Analyze invocation complete without error against a real -// logical.Schema. -func TestDispatch_EmptyResult_FallsThrough(t *testing.T) { +// TestDispatch_EmptyResult_CanonicalEmptyIterator covers G9c #13: an +// eligible request reaches ec.Query, ec returns (nil, nil) (the row +// path's typed-nil empty result). Dispatch emits the canonical empty +// MIterator (Next()==false, Close()==nil) with handled=true — the same +// empty []*measurev1.InternalDataPoint the row iterator +// (resultMIterator{result: nil}) would surface. This also confirms the +// index.Query construction and Analyze invocation complete without error +// against a real logical.Schema. +func TestDispatch_EmptyResult_CanonicalEmptyIterator(t *testing.T) { measureSchema := testMeasureSchema() // nolint:staticcheck // SA1019 — row-path BuildSchema is the only schema builder until G8 replaces it. logicalSchema, schemaErr := logicalmeasure.BuildSchema(measureSchema, nil) @@ -364,19 +429,25 @@ func TestDispatch_EmptyResult_FallsThrough(t *testing.T) { metadata := &commonv1.Metadata{Name: "demo", Group: "default"} ec := &fakeEC{wantResult: nil, wantErr: nil} - iter, planStr, handled, err := Dispatch(context.Background(), + iter, _, handled, err := Dispatch(context.Background(), bareReq(), metadata, measureSchema, logicalSchema, ec, dispatchCfg(true)) if err != nil { t.Fatalf("dispatch must not error on empty result: %v", err) } - if handled { - t.Fatal("empty result must fall through to row path") + if !handled { + t.Fatal("empty result must be handled (canonical empty response, not a fall-through)") } - if iter != nil || planStr != "" { - t.Fatalf("expect zero outputs on fallthrough, got iter=%v planStr=%q", iter, planStr) + if iter == nil { + t.Fatal("expected an empty MIterator, got nil") + } + if iter.Next() { + t.Fatal("empty-result MIterator must report Next()==false") + } + if closeErr := iter.Close(); closeErr != nil { + t.Fatalf("empty-result MIterator Close must be nil (row-path parity): %v", closeErr) } if !ec.called { - t.Fatal("ec.Query must be invoked before fallthrough decision") + t.Fatal("ec.Query must be invoked before the empty-result decision") } if ec.lastOpts.Name != "demo" { t.Fatalf("opts.Name: want demo, got %q", ec.lastOpts.Name) @@ -395,8 +466,11 @@ func TestDispatch_Counters_TrackFellThroughCalls(t *testing.T) { startHandled := HandledCount() startFellThrough := FellThroughCount() - // Three fallthroughs of distinct shapes, each tripping a different - // gate so the counter is exercised across the eligibility branches. + // Three fallthroughs of distinct shapes. topReq trips the Top gate; + // the other two trip the PERMANENT nil-runtime-context guard (all-nil + // schema/ec/metadata). Note: post-G9c a nil TimeRange is no longer a + // fall-through on its own — noTimeReq falls through here only because + // the runtime context is nil. topReq := bareReq() topReq.Top = &measurev1.QueryRequest_Top{Number: 5, FieldName: "value"} noTimeReq := bareReq() diff --git a/test/cases/measure/data/input/empty_result.ql b/test/cases/measure/data/input/empty_result.ql new file mode 100644 index 000000000..1ba070c8c --- /dev/null +++ b/test/cases/measure/data/input/empty_result.ql @@ -0,0 +1,21 @@ +# 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 id, total::field, value::field FROM MEASURE service_cpm_minute IN sw_metric +TIME > '-15m' +WHERE id = 'no_such_id_xyz' diff --git a/test/cases/measure/data/input/empty_result.yaml b/test/cases/measure/data/input/empty_result.yaml new file mode 100644 index 000000000..48b91206e --- /dev/null +++ b/test/cases/measure/data/input/empty_result.yaml @@ -0,0 +1,32 @@ +# 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: "service_cpm_minute" +groups: ["sw_metric"] +tagProjection: + tagFamilies: + - name: "default" + tags: ["id"] +fieldProjection: + names: ["total", "value"] +criteria: + condition: + name: "id" + op: "BINARY_OP_EQ" + value: + str: + value: "no_such_id_xyz" diff --git a/test/cases/measure/data/input/project_non_existent_field.ql b/test/cases/measure/data/input/project_non_existent_field.ql new file mode 100644 index 000000000..0a567dc5f --- /dev/null +++ b/test/cases/measure/data/input/project_non_existent_field.ql @@ -0,0 +1,19 @@ +# 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 id, total, nonexistent_field FROM service_cpm_minute IN sw_metric +TIME > '-15m' diff --git a/test/cases/measure/data/input/project_non_existent_field.yaml b/test/cases/measure/data/input/project_non_existent_field.yaml new file mode 100644 index 000000000..64c7da9e4 --- /dev/null +++ b/test/cases/measure/data/input/project_non_existent_field.yaml @@ -0,0 +1,25 @@ +# 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: "service_cpm_minute" +groups: ["sw_metric"] +tagProjection: + tagFamilies: + - name: "default" + tags: ["id"] +fieldProjection: + names: ["total", "nonexistent_field"] diff --git a/test/cases/measure/measure.go b/test/cases/measure/measure.go index 81127e221..a240092e4 100644 --- a/test/cases/measure/measure.go +++ b/test/cases/measure/measure.go @@ -97,6 +97,7 @@ var measureEntries = []any{ g.Entry("multi groups: new tag and fields", helpers.Args{Input: "multi_group_new_tag_field", Duration: 35 * time.Minute, Offset: -20 * time.Minute}), g.Entry("filter by non-existent tag", helpers.Args{Input: "filter_non_existent_tag", Duration: 25 * time.Minute, Offset: -20 * time.Minute, WantErr: true}), g.Entry("project non-existent tag", helpers.Args{Input: "project_non_existent_tag", Duration: 25 * time.Minute, Offset: -20 * time.Minute, WantErr: true}), + g.Entry("project non-existent field", helpers.Args{Input: "project_non_existent_field", Duration: 25 * time.Minute, Offset: -20 * time.Minute, WantErr: true}), g.Entry("write mixed", helpers.Args{Input: "write_mixed", Duration: 15 * time.Minute, Offset: 25 * time.Minute, DisOrder: true}), g.Entry("filter by tag with NE", helpers.Args{Input: "tag_filter_ne", Duration: 25 * time.Minute, Offset: -20 * time.Minute, DisOrder: true}), g.Entry("filter by tag with NOT IN", helpers.Args{Input: "tag_filter_not_in", Duration: 25 * time.Minute, Offset: -20 * time.Minute, DisOrder: true}), @@ -107,6 +108,7 @@ var measureEntries = []any{ g.Entry("index mode filter by NE", helpers.Args{Input: "index_mode_ne", Duration: 25 * time.Minute, Offset: -20 * time.Minute, DisOrder: true}), g.Entry("index mode filter by LE on int", helpers.Args{Input: "index_mode_le", Duration: 25 * time.Minute, Offset: -20 * time.Minute, DisOrder: true}), g.Entry("offset beyond results", helpers.Args{Input: "offset_empty", Duration: 25 * time.Minute, Offset: -20 * time.Minute, WantEmpty: true}), + g.Entry("empty result for unmatched filter", helpers.Args{Input: "empty_result", Duration: 25 * time.Minute, Offset: -20 * time.Minute, WantEmpty: true}), // Generated test cases (Layer 1: leaf conditions) g.Entry("gen: leaf EQ string", helpers.Args{Input: "gen_leaf_eq_str", Want: "gen_leaf_eq_str", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
