You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by ha...@apache.org on 2026/04/15 06:42:57 UTC
(skywalking-banyandb) 04/04: refactor(query): extract validation helpers to reduce cyclomatic complexity
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch test/query
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit d53c651e40bacdcf3e658108108af6b62dea6fbf
Author: Hongtao Gao <ha...@gmail.com>
AuthorDate: Wed Apr 15 05:53:10 2026 +0000
refactor(query): extract validation helpers to reduce cyclomatic complexity
Extract allEntitiesSpecific, validateMatchCondition, and validateINCondition
helpers from BuildQuery and parseConditionToQuery, bringing both below the
gocyclo threshold of 30. Also fix remaining lint issues in the test-case
generator: gosec G306 (0o600 perms), goconst (true/false/none constants),
gocritic (exitAfterDefer, appendCombine), line-length wrapping, and package
comments.
via [HAPI](https://hapi.run)
---
pkg/index/inverted/query.go | 75 +++++++++++++---------
test/cases/measure/cmd/capture/capture_test.go | 2 +-
test/cases/measure/cmd/generate/capture.go | 12 ++--
test/cases/measure/cmd/generate/entry.go | 17 +++--
test/cases/measure/cmd/generate/gap.go | 22 +++----
test/cases/measure/cmd/generate/layer1_leaf.go | 44 +++++++------
test/cases/measure/cmd/generate/layer2_tree.go | 14 ++--
test/cases/measure/cmd/generate/layer3_features.go | 34 +++++-----
test/cases/measure/cmd/generate/main.go | 6 +-
test/cases/measure/cmd/generate/ql_renderer.go | 15 ++---
test/cases/measure/cmd/generate/seed.go | 34 +++++++---
test/cases/measure/cmd/generate/types.go | 11 ++--
test/cases/measure/measure.go | 63 ++++++++++++------
13 files changed, 208 insertions(+), 141 deletions(-)
diff --git a/pkg/index/inverted/query.go b/pkg/index/inverted/query.go
index 1221e4643..8018ec6f4 100644
--- a/pkg/index/inverted/query.go
+++ b/pkg/index/inverted/query.go
@@ -150,25 +150,11 @@ func BuildQuery(criteria *modelv1.Criteria, schema logical.Schema, entityDict ma
// When one OR branch is entity-only (nil query), entity-level series lookup already
// constrains the scope. If all entities are specific (no AnyTagValue wildcards),
// return match-all — no index query is needed.
- if left == nil || right == nil {
- allSpecific := true
- for _, entity := range entities {
- for _, entry := range entity {
- if entry == pbv1.AnyTagValue {
- allSpecific = false
- break
- }
- }
- if !allSpecific {
- break
- }
- }
- if allSpecific {
- return &queryNode{
- query: bluge.NewMatchAllQuery(),
- node: newMatchAllNode(),
- }, entities, true, nil
- }
+ if (left == nil || right == nil) && allEntitiesSpecific(entities) {
+ return &queryNode{
+ query: bluge.NewMatchAllQuery(),
+ node: newMatchAllNode(),
+ }, entities, true, nil
}
query, node := bluge.NewBooleanQuery(), newShouldNode()
query.SetMinShould(1)
@@ -187,6 +173,18 @@ func BuildQuery(criteria *modelv1.Criteria, schema logical.Schema, entityDict ma
return nil, nil, false, logical.ErrInvalidCriteriaType
}
+// allEntitiesSpecific reports whether every entry in all entity lists is a concrete value (no AnyTagValue wildcard).
+func allEntitiesSpecific(entities [][]*modelv1.TagValue) bool {
+ for _, entity := range entities {
+ for _, entry := range entity {
+ if entry == pbv1.AnyTagValue {
+ return false
+ }
+ }
+ }
+ return true
+}
+
// BuildIndexModeQuery returns blugeQuery for index mode.
func BuildIndexModeQuery(measureName string, criteria *modelv1.Criteria, schema logical.Schema) (index.Query, error) {
var subjectQuery bluge.Query
@@ -290,6 +288,28 @@ func buildIndexModeCriteria(criteria *modelv1.Criteria, schema logical.Schema, e
return nil, logical.ErrInvalidCriteriaType
}
+func validateMatchCondition(cond *modelv1.Condition) error {
+ if cond.Value == nil {
+ return errors.WithMessagef(logical.ErrUnsupportedConditionValue, "MATCH condition requires non-nil value")
+ }
+ if _, ok := cond.Value.Value.(*modelv1.TagValue_Str); !ok {
+ return errors.WithMessagef(logical.ErrUnsupportedConditionValue, "MATCH condition requires string value type: %s", cond)
+ }
+ return nil
+}
+
+func validateINCondition(cond *modelv1.Condition) error {
+ if cond.Value == nil {
+ return errors.WithMessagef(logical.ErrUnsupportedConditionValue, "IN condition requires non-nil value")
+ }
+ switch cond.Value.Value.(type) {
+ case *modelv1.TagValue_StrArray, *modelv1.TagValue_IntArray:
+ return nil
+ default:
+ return errors.WithMessagef(logical.ErrUnsupportedConditionValue, "IN condition requires array value type: %s", cond)
+ }
+}
+
func parseConditionToQuery(cond *modelv1.Condition, indexRule *databasev1.IndexRule,
expr logical.LiteralExpr, fieldKey string,
) (*queryNode, error) {
@@ -339,11 +359,8 @@ func parseConditionToQuery(cond *modelv1.Condition, indexRule *databasev1.IndexR
if indexRule == nil {
return nil, errors.WithMessagef(logical.ErrUnsupportedConditionOp, "index rule is mandatory for match operation: %s", cond)
}
- if cond.Value == nil {
- return nil, errors.WithMessagef(logical.ErrUnsupportedConditionValue, "MATCH condition requires non-nil value")
- }
- if _, ok := cond.Value.Value.(*modelv1.TagValue_Str); !ok {
- return nil, errors.WithMessagef(logical.ErrUnsupportedConditionValue, "MATCH condition requires string value type: %s", cond)
+ if matchErr := validateMatchCondition(cond); matchErr != nil {
+ return nil, matchErr
}
bb := expr.Bytes()
if len(bb) != 1 {
@@ -386,14 +403,8 @@ func parseConditionToQuery(cond *modelv1.Condition, indexRule *databasev1.IndexR
node.SetSubNode(node)
return &queryNode{query, node}, nil
case modelv1.Condition_BINARY_OP_IN:
- if cond.Value == nil {
- return nil, errors.WithMessagef(logical.ErrUnsupportedConditionValue, "IN condition requires non-nil value")
- }
- switch cond.Value.Value.(type) {
- case *modelv1.TagValue_StrArray, *modelv1.TagValue_IntArray:
- // valid
- default:
- return nil, errors.WithMessagef(logical.ErrUnsupportedConditionValue, "IN condition requires array value type: %s", cond)
+ if inErr := validateINCondition(cond); inErr != nil {
+ return nil, inErr
}
bb, elements := expr.Bytes(), expr.Elements()
query, node := bluge.NewBooleanQuery(), newShouldNode()
diff --git a/test/cases/measure/cmd/capture/capture_test.go b/test/cases/measure/cmd/capture/capture_test.go
index 113d479f4..d5d80a85d 100644
--- a/test/cases/measure/cmd/capture/capture_test.go
+++ b/test/cases/measure/cmd/capture/capture_test.go
@@ -169,7 +169,7 @@ func TestCapture(t *testing.T) {
}
wantPath := filepath.Join(wantDirPath, testName+".yaml")
- if writeErr := os.WriteFile(wantPath, respYAML, 0o644); writeErr != nil {
+ if writeErr := os.WriteFile(wantPath, respYAML, 0o600); writeErr != nil {
t.Errorf(" [ERROR] %s: write failed: %v", testName, writeErr)
continue
}
diff --git a/test/cases/measure/cmd/generate/capture.go b/test/cases/measure/cmd/generate/capture.go
index 91bd4c2de..f4b79dc0a 100644
--- a/test/cases/measure/cmd/generate/capture.go
+++ b/test/cases/measure/cmd/generate/capture.go
@@ -15,6 +15,9 @@
// specific language governing permissions and limitations
// under the License.
+// Package main implements the capture subcommand for the test-case generator.
+// It connects to a live BanyanDB server, executes generated queries, and
+// writes the responses as want/*.yaml files.
package main
import (
@@ -55,16 +58,12 @@ func runCapture(outputDir, serverAddr string) {
fmt.Fprintf(os.Stderr, "Error connecting to server: %v\n", connErr)
os.Exit(1)
}
- defer func() {
- if closeErr := conn.Close(); closeErr != nil {
- fmt.Fprintf(os.Stderr, "Warning: error closing connection: %v\n", closeErr)
- }
- }()
// Discover all generated input YAML files
entries, readErr := os.ReadDir(inputDirPath)
if readErr != nil {
fmt.Fprintf(os.Stderr, "Error reading input directory: %v\n", readErr)
+ _ = conn.Close()
os.Exit(1)
}
@@ -166,7 +165,7 @@ func runCapture(outputDir, serverAddr string) {
}
wantPath := filepath.Join(wantDirPath, testName+".yaml")
- if writeErr := os.WriteFile(wantPath, respYAML, 0o644); writeErr != nil {
+ if writeErr := os.WriteFile(wantPath, respYAML, 0o600); writeErr != nil {
fmt.Fprintf(os.Stderr, " [ERROR] %s: write failed: %v\n", testName, writeErr)
errorCount++
continue
@@ -177,6 +176,7 @@ func runCapture(outputDir, serverAddr string) {
capturedCount++
}
+ _ = conn.Close()
fmt.Printf("\nCapture complete: %d captured, %d skipped (error cases), %d errors\n",
capturedCount, skippedCount, errorCount)
}
diff --git a/test/cases/measure/cmd/generate/entry.go b/test/cases/measure/cmd/generate/entry.go
index ac77b7f48..3058b63d9 100644
--- a/test/cases/measure/cmd/generate/entry.go
+++ b/test/cases/measure/cmd/generate/entry.go
@@ -22,6 +22,11 @@ import (
"strings"
)
+const (
+ strTrue = "true"
+ strFalse = "false"
+)
+
// GenerateEntryCode produces g.Entry lines for measure.go.
func GenerateEntryCode(cases []*TestCase) string {
var lines []string
@@ -38,20 +43,20 @@ func GenerateEntryCode(cases []*TestCase) string {
func renderEntryLine(tc *TestCase) string {
name := tc.Name
want := name
- wantEmpty := "false"
- wantErr := "false"
- disOrder := "false"
+ wantEmpty := strFalse
+ wantErr := strFalse
+ disOrder := strFalse
duration := "25 * time.Minute"
offset := "-20 * time.Minute"
if tc.WantErr {
- wantErr = "true"
+ wantErr = strTrue
}
if tc.WantEmpty {
- wantEmpty = "true"
+ wantEmpty = strTrue
}
if tc.DisOrder {
- disOrder = "true"
+ disOrder = strTrue
}
if tc.Duration != "" {
duration = tc.Duration
diff --git a/test/cases/measure/cmd/generate/gap.go b/test/cases/measure/cmd/generate/gap.go
index 1aa5e78ef..8e0be9475 100644
--- a/test/cases/measure/cmd/generate/gap.go
+++ b/test/cases/measure/cmd/generate/gap.go
@@ -26,17 +26,17 @@ import (
// CoverageMap tracks which combinations are already tested by existing cases.
type CoverageMap struct {
- BinaryOps map[string]bool // "EQ:str", "NE:int", etc.
- AggFunctions map[string]bool // "MEAN", "MAX", etc.
- TreeDepths map[int]bool // 1, 2, 3, 5
- LogicalOps map[string]bool // "AND", "OR"
- SortOrders map[string]bool // "ASC", "DESC"
- TopN bool
- GroupBy bool
- Limit bool
- Offset bool
- Criteria map[string]bool // "EQ", "NE", "IN", etc. (any use of criteria)
- TestNames map[string]bool // existing test names
+ BinaryOps map[string]bool
+ AggFunctions map[string]bool
+ TreeDepths map[int]bool
+ LogicalOps map[string]bool
+ SortOrders map[string]bool
+ Criteria map[string]bool
+ TestNames map[string]bool
+ TopN bool
+ GroupBy bool
+ Limit bool
+ Offset bool
}
// AnalyzeGaps reads existing test files and builds a coverage map.
diff --git a/test/cases/measure/cmd/generate/layer1_leaf.go b/test/cases/measure/cmd/generate/layer1_leaf.go
index 51b0c3e33..5c155df1f 100644
--- a/test/cases/measure/cmd/generate/layer1_leaf.go
+++ b/test/cases/measure/cmd/generate/layer1_leaf.go
@@ -26,12 +26,12 @@ import (
// LeafOp defines a valid BinaryOp + TagValue + measure + tag combination.
type LeafOp struct {
- Op modelv1.Condition_BinaryOp
- ValueType string // "str", "int", "null", "str_array", "int_array"
- Value *modelv1.TagValue // example value to use
- Measure string // target measure name
- TagName string // tag to filter on
- QLValue string // QL representation of the value
+ Value *modelv1.TagValue
+ ValueType string
+ Measure string
+ TagName string
+ QLValue string
+ Op modelv1.Condition_BinaryOp
}
// GenerateLayer1 produces test cases for the Criteria leaf layer.
@@ -50,10 +50,10 @@ func GenerateLayer1() []*TestCase {
continue
}
cases = append(cases, &TestCase{
- Name: fmt.Sprintf("gen_leaf_%s_%s", binaryOpShortName(op.Op), op.ValueType),
- Measure: m,
- Request: req,
- QL: ql,
+ Name: fmt.Sprintf("gen_leaf_%s_%s", binaryOpShortName(op.Op), op.ValueType),
+ Measure: m,
+ Request: req,
+ QL: ql,
WantErr: op.ValueType == "null", // server rejects null value comparisons
Duration: "25 * time.Minute",
})
@@ -89,10 +89,16 @@ func defineLeafOps() []LeafOp {
{Op: modelv1.Condition_BINARY_OP_GE, ValueType: "str", Value: TagValueStr("svc2"), Measure: "service_cpm_minute", TagName: "id", QLValue: "'svc2'"},
{Op: modelv1.Condition_BINARY_OP_GE, ValueType: "int", Value: TagValueInt(2), Measure: "service_traffic", TagName: "layer", QLValue: "2"},
// IN with str_array, int_array
- {Op: modelv1.Condition_BINARY_OP_IN, ValueType: "str_array", Value: TagValueStrArray([]string{"svc1", "svc2"}), Measure: "service_cpm_minute", TagName: "id", QLValue: "('svc1', 'svc2')"},
+ {
+ Op: modelv1.Condition_BINARY_OP_IN, ValueType: "str_array", Value: TagValueStrArray([]string{"svc1", "svc2"}),
+ Measure: "service_cpm_minute", TagName: "id", QLValue: "('svc1', 'svc2')",
+ },
{Op: modelv1.Condition_BINARY_OP_IN, ValueType: "int_array", Value: TagValueIntArray([]int64{1, 2}), Measure: "service_traffic", TagName: "layer", QLValue: "(1, 2)"},
// NOT_IN with str_array, int_array
- {Op: modelv1.Condition_BINARY_OP_NOT_IN, ValueType: "str_array", Value: TagValueStrArray([]string{"svc3"}), Measure: "service_cpm_minute", TagName: "id", QLValue: "('svc3')"},
+ {
+ Op: modelv1.Condition_BINARY_OP_NOT_IN, ValueType: "str_array", Value: TagValueStrArray([]string{"svc3"}),
+ Measure: "service_cpm_minute", TagName: "id", QLValue: "('svc3')",
+ },
{Op: modelv1.Condition_BINARY_OP_NOT_IN, ValueType: "int_array", Value: TagValueIntArray([]int64{0}), Measure: "service_traffic", TagName: "layer", QLValue: "(0)"},
// MATCH with str
{Op: modelv1.Condition_BINARY_OP_MATCH, ValueType: "str", Value: TagValueStr("nodea"), Measure: "service_instance_traffic", TagName: "name", QLValue: "MATCH('nodea')"},
@@ -105,10 +111,10 @@ func buildLeafRequest(m *Measure, op LeafOp) *measurev1.QueryRequest {
tagNames := AllTagNames(m)
fieldNames := AllFieldNames(m)
return &measurev1.QueryRequest{
- Name: m.Name,
- Groups: []string{m.Group},
- Criteria: criteria,
- TagProjection: BuildTagProjection(m, tagNames),
+ Name: m.Name,
+ Groups: []string{m.Group},
+ Criteria: criteria,
+ TagProjection: BuildTagProjection(m, tagNames),
FieldProjection: BuildFieldProjection(fieldNames),
}
}
@@ -117,7 +123,7 @@ func generateLeafErrorCases() []*TestCase {
return []*TestCase{
// IN with scalar (should use array)
{
- Name: "gen_err_in_scalar",
+ Name: "gen_err_in_scalar",
Measure: FindMeasure("service_cpm_minute"),
Request: &measurev1.QueryRequest{
Name: "service_cpm_minute",
@@ -125,7 +131,7 @@ func generateLeafErrorCases() []*TestCase {
Criteria: BuildCriteriaFromCondition(
BuildCondition("id", modelv1.Condition_BINARY_OP_IN, TagValueStr("svc1")),
),
- TagProjection: BuildTagProjection(FindMeasure("service_cpm_minute"), []string{"id", "entity_id"}),
+ TagProjection: BuildTagProjection(FindMeasure("service_cpm_minute"), []string{"id", "entity_id"}),
FieldProjection: BuildFieldProjection([]string{"total", "value"}),
},
QL: "SELECT id, entity_id, total, value FROM MEASURE service_cpm_minute IN sw_metric TIME > '-15m' WHERE id IN ('svc1')",
@@ -133,7 +139,7 @@ func generateLeafErrorCases() []*TestCase {
},
// MATCH with int value
{
- Name: "gen_err_match_int",
+ Name: "gen_err_match_int",
Measure: FindMeasure("service_traffic"),
Request: &measurev1.QueryRequest{
Name: "service_traffic",
diff --git a/test/cases/measure/cmd/generate/layer2_tree.go b/test/cases/measure/cmd/generate/layer2_tree.go
index 44a54adb3..240683e01 100644
--- a/test/cases/measure/cmd/generate/layer2_tree.go
+++ b/test/cases/measure/cmd/generate/layer2_tree.go
@@ -33,9 +33,9 @@ func GenerateLayer2() []*TestCase {
}
type depthConfig struct {
- depth int
- rootOp modelv1.LogicalExpression_LogicalOp
- suffix string
+ suffix string
+ depth int
+ rootOp modelv1.LogicalExpression_LogicalOp
}
configs := []depthConfig{
@@ -55,10 +55,10 @@ func GenerateLayer2() []*TestCase {
continue
}
req := &measurev1.QueryRequest{
- Name: m.Name,
- Groups: []string{m.Group},
- Criteria: criteria,
- TagProjection: BuildTagProjection(m, []string{"id", "entity_id"}),
+ Name: m.Name,
+ Groups: []string{m.Group},
+ Criteria: criteria,
+ TagProjection: BuildTagProjection(m, []string{"id", "entity_id"}),
FieldProjection: BuildFieldProjection([]string{"total", "value"}),
}
ql, qlErr := RenderQL(req)
diff --git a/test/cases/measure/cmd/generate/layer3_features.go b/test/cases/measure/cmd/generate/layer3_features.go
index d92d89e51..4670347fb 100644
--- a/test/cases/measure/cmd/generate/layer3_features.go
+++ b/test/cases/measure/cmd/generate/layer3_features.go
@@ -25,6 +25,8 @@ import (
modelv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/model/v1"
)
+const strNone = "none"
+
// GenerateLayer3 produces test cases for query feature combinations.
// Uses pairwise testing across AggFunction, TopN, GroupBy, OrderBy, and CriteriaPresence.
func GenerateLayer3() []*TestCase {
@@ -34,31 +36,31 @@ func GenerateLayer3() []*TestCase {
}
params := map[string][]string{
- "agg": {"MEAN", "MAX", "MIN", "COUNT", "SUM", "none"},
- "top": {"desc", "asc", "none"},
+ "agg": {"MEAN", "MAX", "MIN", "COUNT", "SUM", strNone},
+ "top": {"desc", "asc", strNone},
"group": {"true", "false"},
- "order": {"asc", "desc", "none"},
- "filter": {"none", "eq"},
+ "order": {"asc", "desc", strNone},
+ "filter": {strNone, "eq"},
}
constraints := []ConstraintFunc{
// TopN requires GroupBy
func(tv TestVector) bool {
- if tv["top"] != "none" && tv["group"] == "false" {
+ if tv["top"] != strNone && tv["group"] == "false" {
return false
}
return true
},
// Agg (non-none) requires GroupBy
func(tv TestVector) bool {
- if tv["agg"] != "none" && tv["group"] == "false" {
+ if tv["agg"] != strNone && tv["group"] == "false" {
return false
}
return true
},
// TopN requires Agg
func(tv TestVector) bool {
- if tv["top"] != "none" && tv["agg"] == "none" {
+ if tv["top"] != strNone && tv["agg"] == strNone {
return false
}
return true
@@ -80,21 +82,21 @@ func GenerateLayer3() []*TestCase {
continue
}
- disOrder := tv["order"] == "none" && tv["group"] == "true"
+ disOrder := tv["order"] == strNone && tv["group"] == "true"
nameParts := []string{}
- if tv["agg"] != "none" {
+ if tv["agg"] != strNone {
nameParts = append(nameParts, strings.ToLower(tv["agg"]))
}
- if tv["top"] != "none" {
+ if tv["top"] != strNone {
nameParts = append(nameParts, "top_"+tv["top"])
}
if tv["group"] == "true" {
nameParts = append(nameParts, "group")
}
- if tv["order"] != "none" {
+ if tv["order"] != strNone {
nameParts = append(nameParts, "order_"+tv["order"])
}
- if tv["filter"] != "none" {
+ if tv["filter"] != strNone {
nameParts = append(nameParts, "filter")
}
if len(nameParts) == 0 {
@@ -139,7 +141,7 @@ func buildLayer3Request(m *Measure, tv TestVector) *measurev1.QueryRequest {
}
// Aggregation
- if tv["agg"] != "none" {
+ if tv["agg"] != strNone {
req.Agg = &measurev1.QueryRequest_Aggregation{
Function: parseAggFunction(tv["agg"]),
FieldName: "value",
@@ -147,7 +149,7 @@ func buildLayer3Request(m *Measure, tv TestVector) *measurev1.QueryRequest {
}
// TopN
- if tv["top"] != "none" {
+ if tv["top"] != strNone {
sortVal := modelv1.Sort_SORT_DESC
if tv["top"] == "asc" {
sortVal = modelv1.Sort_SORT_ASC
@@ -200,10 +202,10 @@ func ensureAggCoverage(vectors []TestVector) []TestVector {
// Add a vector with this agg function, group=true, varied other params
vectors = append(vectors, TestVector{
"agg": aggFn,
- "top": "none",
+ "top": strNone,
"group": "true",
"order": "desc",
- "filter": "none",
+ "filter": strNone,
})
}
return vectors
diff --git a/test/cases/measure/cmd/generate/main.go b/test/cases/measure/cmd/generate/main.go
index 46140ac63..4efc19276 100644
--- a/test/cases/measure/cmd/generate/main.go
+++ b/test/cases/measure/cmd/generate/main.go
@@ -100,7 +100,7 @@ func runGenerate(outputDir string) {
for _, tc := range filtered {
// Write .ql file
qlPath := filepath.Join(inputDirPath, tc.Name+".ql")
- if writeErr := os.WriteFile(qlPath, tc.QLFileContent(), 0o644); writeErr != nil {
+ if writeErr := os.WriteFile(qlPath, tc.QLFileContent(), 0o600); writeErr != nil {
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", qlPath, writeErr)
continue
}
@@ -112,7 +112,7 @@ func runGenerate(outputDir string) {
continue
}
yamlPath := filepath.Join(inputDirPath, tc.Name+".yaml")
- if writeErr := os.WriteFile(yamlPath, yamlContent, 0o644); writeErr != nil {
+ if writeErr := os.WriteFile(yamlPath, yamlContent, 0o600); writeErr != nil {
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", yamlPath, writeErr)
continue
}
@@ -122,7 +122,7 @@ func runGenerate(outputDir string) {
// Write placeholder want file (to be filled by capture)
wantPath := filepath.Join(wantDirPath, tc.Name+".yaml")
placeholder := []byte("# Placeholder - run 'capture' subcommand to fill this file\n")
- if writeErr := os.WriteFile(wantPath, placeholder, 0o644); writeErr != nil {
+ if writeErr := os.WriteFile(wantPath, placeholder, 0o600); writeErr != nil {
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", wantPath, writeErr)
continue
}
diff --git a/test/cases/measure/cmd/generate/ql_renderer.go b/test/cases/measure/cmd/generate/ql_renderer.go
index 15a8e55d6..c8dc26e76 100644
--- a/test/cases/measure/cmd/generate/ql_renderer.go
+++ b/test/cases/measure/cmd/generate/ql_renderer.go
@@ -31,10 +31,11 @@ func RenderQL(req *measurev1.QueryRequest) (string, error) {
// SELECT projection
projection := renderProjection(req)
- parts = append(parts, fmt.Sprintf("SELECT %s FROM MEASURE %s IN %s", projection, req.GetName(), strings.Join(req.GetGroups(), ",")))
-
// TIME — always required by the BydbQL service; actual range is injected by the test framework
- parts = append(parts, "TIME > '-15m'")
+ parts = append(parts,
+ fmt.Sprintf("SELECT %s FROM MEASURE %s IN %s", projection, req.GetName(), strings.Join(req.GetGroups(), ",")),
+ "TIME > '-15m'",
+ )
// WHERE
if req.GetCriteria() != nil {
@@ -50,9 +51,7 @@ func RenderQL(req *measurev1.QueryRequest) (string, error) {
var groupCols []string
if tp := req.GetGroupBy().GetTagProjection(); tp != nil {
for _, family := range tp.GetTagFamilies() {
- for _, tag := range family.GetTags() {
- groupCols = append(groupCols, tag)
- }
+ groupCols = append(groupCols, family.GetTags()...)
}
}
if fn := req.GetGroupBy().GetFieldName(); fn != "" {
@@ -99,9 +98,7 @@ func renderProjection(req *measurev1.QueryRequest) string {
// Tag projection
if tp := req.GetTagProjection(); tp != nil {
for _, family := range tp.GetTagFamilies() {
- for _, tag := range family.GetTags() {
- cols = append(cols, tag)
- }
+ cols = append(cols, family.GetTags()...)
}
}
diff --git a/test/cases/measure/cmd/generate/seed.go b/test/cases/measure/cmd/generate/seed.go
index 8bd72b965..2f76e2370 100644
--- a/test/cases/measure/cmd/generate/seed.go
+++ b/test/cases/measure/cmd/generate/seed.go
@@ -38,9 +38,9 @@ const (
// TagDef defines a tag in a measure schema.
type TagDef struct {
Name string
+ Family string
Type TagType
- Family string // tag family name (e.g., "default")
- IsEntity bool // whether this tag is the entity
+ IsEntity bool
}
// FieldDef defines a field in a measure schema.
@@ -124,9 +124,18 @@ func serviceTraffic() Measure {
},
Fields: []FieldDef{},
DataPoints: []DataPoint{
- {TagValues: map[string]interface{}{"id": "1", "service_id": "service_1", "name": "service_name_1", "short_name": "service_short_name_1", "service_group": "group1", "layer": int64(1)}},
- {TagValues: map[string]interface{}{"id": "2", "service_id": "service_2", "name": "service_name_2", "short_name": "service_short_name_2", "service_group": "group1", "layer": int64(2)}},
- {TagValues: map[string]interface{}{"id": "3", "service_id": "service_3", "name": "service_name_3", "short_name": "service_short_name_3", "service_group": "group1", "layer": int64(0)}},
+ {TagValues: map[string]interface{}{
+ "id": "1", "service_id": "service_1", "name": "service_name_1",
+ "short_name": "service_short_name_1", "service_group": "group1", "layer": int64(1),
+ }},
+ {TagValues: map[string]interface{}{
+ "id": "2", "service_id": "service_2", "name": "service_name_2",
+ "short_name": "service_short_name_2", "service_group": "group1", "layer": int64(2),
+ }},
+ {TagValues: map[string]interface{}{
+ "id": "3", "service_id": "service_3", "name": "service_name_3",
+ "short_name": "service_short_name_3", "service_group": "group1", "layer": int64(0),
+ }},
},
}
}
@@ -165,9 +174,18 @@ func instanceCPUCPUMinute() Measure {
{Name: "value", Kind: FieldKindFloat},
},
DataPoints: []DataPoint{
- {TagValues: map[string]interface{}{"service_id": "1", "entity_id": "entity_1"}, FieldValues: map[string]interface{}{"summation": 18.1, "count": int64(1), "value": 18.1}},
- {TagValues: map[string]interface{}{"service_id": "4", "entity_id": "entity_2"}, FieldValues: map[string]interface{}{"summation": 100.4, "count": int64(2), "value": 50.2}},
- {TagValues: map[string]interface{}{"service_id": "5", "entity_id": "entity_2"}, FieldValues: map[string]interface{}{"summation": 100.0, "count": int64(3), "value": 33.333}},
+ {
+ TagValues: map[string]interface{}{"service_id": "1", "entity_id": "entity_1"},
+ FieldValues: map[string]interface{}{"summation": 18.1, "count": int64(1), "value": 18.1},
+ },
+ {
+ TagValues: map[string]interface{}{"service_id": "4", "entity_id": "entity_2"},
+ FieldValues: map[string]interface{}{"summation": 100.4, "count": int64(2), "value": 50.2},
+ },
+ {
+ TagValues: map[string]interface{}{"service_id": "5", "entity_id": "entity_2"},
+ FieldValues: map[string]interface{}{"summation": 100.0, "count": int64(3), "value": 33.333},
+ },
},
}
}
diff --git a/test/cases/measure/cmd/generate/types.go b/test/cases/measure/cmd/generate/types.go
index b4f33f2ed..20f11fc1e 100644
--- a/test/cases/measure/cmd/generate/types.go
+++ b/test/cases/measure/cmd/generate/types.go
@@ -18,22 +18,23 @@
package main
import (
- measurev1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/measure/v1"
- modelv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/model/v1"
"google.golang.org/protobuf/encoding/protojson"
"sigs.k8s.io/yaml"
+
+ measurev1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/measure/v1"
+ modelv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/model/v1"
)
// TestCase represents a generated test case with all its artifacts.
type TestCase struct {
- Name string
Measure *Measure
Request *measurev1.QueryRequest
- QL string // rendered QL query string
+ Name string
+ QL string
+ Duration string
WantErr bool
WantEmpty bool
DisOrder bool
- Duration string // Go duration expression for helpers.Args
}
const licenseHeader = "# Licensed to Apache Software Foundation (ASF) under one or more contributor\n" +
diff --git a/test/cases/measure/measure.go b/test/cases/measure/measure.go
index 43ad0617c..c948ff5eb 100644
--- a/test/cases/measure/measure.go
+++ b/test/cases/measure/measure.go
@@ -111,37 +111,64 @@ var measureEntries = []any{
g.Entry("gen: leaf LE string", helpers.Args{Input: "gen_leaf_le_str", Want: "gen_leaf_le_str", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
g.Entry("gen: leaf GE string", helpers.Args{Input: "gen_leaf_ge_str", Want: "gen_leaf_ge_str", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
g.Entry("gen: leaf IN str array", helpers.Args{Input: "gen_leaf_in_str_array", Want: "gen_leaf_in_str_array", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
- g.Entry("gen: leaf NOT IN str array", helpers.Args{Input: "gen_leaf_not_in_str_array", Want: "gen_leaf_not_in_str_array", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
+ g.Entry("gen: leaf NOT IN str array",
+ helpers.Args{Input: "gen_leaf_not_in_str_array", Want: "gen_leaf_not_in_str_array", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
g.Entry("gen: leaf EQ int", helpers.Args{Input: "gen_leaf_eq_int", Want: "gen_leaf_eq_int", Duration: 25 * time.Minute, Offset: -20 * time.Minute, DisOrder: true}),
g.Entry("gen: leaf NE int", helpers.Args{Input: "gen_leaf_ne_int", Want: "gen_leaf_ne_int", Duration: 25 * time.Minute, Offset: -20 * time.Minute, DisOrder: true}),
g.Entry("gen: leaf LT int", helpers.Args{Input: "gen_leaf_lt_int", Want: "gen_leaf_lt_int", Duration: 25 * time.Minute, Offset: -20 * time.Minute, DisOrder: true}),
g.Entry("gen: leaf GT int", helpers.Args{Input: "gen_leaf_gt_int", Want: "gen_leaf_gt_int", Duration: 25 * time.Minute, Offset: -20 * time.Minute, DisOrder: true}),
g.Entry("gen: leaf LE int", helpers.Args{Input: "gen_leaf_le_int", Want: "gen_leaf_le_int", Duration: 25 * time.Minute, Offset: -20 * time.Minute, DisOrder: true}),
g.Entry("gen: leaf GE int", helpers.Args{Input: "gen_leaf_ge_int", Want: "gen_leaf_ge_int", Duration: 25 * time.Minute, Offset: -20 * time.Minute, DisOrder: true}),
- g.Entry("gen: leaf IN int array", helpers.Args{Input: "gen_leaf_in_int_array", Want: "gen_leaf_in_int_array", Duration: 25 * time.Minute, Offset: -20 * time.Minute, DisOrder: true}),
- g.Entry("gen: leaf NOT IN int array", helpers.Args{Input: "gen_leaf_not_in_int_array", Want: "gen_leaf_not_in_int_array", Duration: 25 * time.Minute, Offset: -20 * time.Minute, DisOrder: true}),
+ g.Entry("gen: leaf IN int array",
+ helpers.Args{Input: "gen_leaf_in_int_array", Want: "gen_leaf_in_int_array", Duration: 25 * time.Minute, Offset: -20 * time.Minute, DisOrder: true}),
+ g.Entry("gen: leaf NOT IN int array",
+ helpers.Args{Input: "gen_leaf_not_in_int_array", Want: "gen_leaf_not_in_int_array", Duration: 25 * time.Minute, Offset: -20 * time.Minute, DisOrder: true}),
g.Entry("gen: leaf MATCH string", helpers.Args{Input: "gen_leaf_match_str", Want: "gen_leaf_match_str", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
g.Entry("gen: err IN with scalar value", helpers.Args{Input: "gen_err_in_scalar", Duration: 25 * time.Minute, Offset: -20 * time.Minute, WantErr: true}),
g.Entry("gen: err MATCH on int field", helpers.Args{Input: "gen_err_match_int", Duration: 25 * time.Minute, Offset: -20 * time.Minute, WantErr: true}),
// Generated test cases (Layer 2: tree conditions)
g.Entry("gen: tree depth 1 leaf", helpers.Args{Input: "gen_tree_depth1_leaf", Want: "gen_tree_depth1_leaf", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
- g.Entry("gen: tree depth 2 AND", helpers.Args{Input: "gen_tree_depth2_and", Want: "gen_tree_depth2_and", DisOrder: true, Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
- g.Entry("gen: tree depth 2 OR", helpers.Args{Input: "gen_tree_depth2_or", Want: "gen_tree_depth2_or", DisOrder: true, Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
- g.Entry("gen: tree depth 3 AND/OR", helpers.Args{Input: "gen_tree_depth3_and_or", Want: "gen_tree_depth3_and_or", DisOrder: true, Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
- g.Entry("gen: tree depth 3 OR/AND", helpers.Args{Input: "gen_tree_depth3_or_and", Want: "gen_tree_depth3_or_and", DisOrder: true, Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
- g.Entry("gen: tree depth 5 deep AND", helpers.Args{Input: "gen_tree_depth5_deep_and", Want: "gen_tree_depth5_deep_and", DisOrder: true, Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
+ g.Entry("gen: tree depth 2 AND",
+ helpers.Args{Input: "gen_tree_depth2_and", Want: "gen_tree_depth2_and", DisOrder: true, Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
+ g.Entry("gen: tree depth 2 OR",
+ helpers.Args{Input: "gen_tree_depth2_or", Want: "gen_tree_depth2_or", DisOrder: true, Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
+ g.Entry("gen: tree depth 3 AND/OR",
+ helpers.Args{Input: "gen_tree_depth3_and_or", Want: "gen_tree_depth3_and_or", DisOrder: true, Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
+ g.Entry("gen: tree depth 3 OR/AND",
+ helpers.Args{Input: "gen_tree_depth3_or_and", Want: "gen_tree_depth3_or_and", DisOrder: true, Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
+ g.Entry("gen: tree depth 5 deep AND",
+ helpers.Args{Input: "gen_tree_depth5_deep_and", Want: "gen_tree_depth5_deep_and", DisOrder: true, Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
g.Entry("gen: tree depth 5 deep OR", helpers.Args{Input: "gen_tree_depth5_deep_or", WantErr: true, Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
// Generated test cases (Layer 3: feature combinations)
- g.Entry("gen: mean top desc group order asc", helpers.Args{Input: "gen_feat_mean_top_desc_group_order_asc_0", Want: "gen_feat_mean_top_desc_group_order_asc_0", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
- g.Entry("gen: mean top asc group order desc filter", helpers.Args{Input: "gen_feat_mean_top_asc_group_order_desc_filter_1", Want: "gen_feat_mean_top_asc_group_order_desc_filter_1", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
- g.Entry("gen: mean group", helpers.Args{Input: "gen_feat_mean_group_2", Want: "gen_feat_mean_group_2", DisOrder: true, Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
- g.Entry("gen: mean top desc group order desc", helpers.Args{Input: "gen_feat_mean_top_desc_group_order_desc_3", Want: "gen_feat_mean_top_desc_group_order_desc_3", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
- g.Entry("gen: mean top asc group order asc", helpers.Args{Input: "gen_feat_mean_top_asc_group_order_asc_4", Want: "gen_feat_mean_top_asc_group_order_asc_4", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
- g.Entry("gen: mean group order asc", helpers.Args{Input: "gen_feat_mean_group_order_asc_5", Want: "gen_feat_mean_group_order_asc_5", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
- g.Entry("gen: max group order desc", helpers.Args{Input: "gen_feat_max_group_order_desc_6", Want: "gen_feat_max_group_order_desc_6", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
- g.Entry("gen: min group order desc", helpers.Args{Input: "gen_feat_min_group_order_desc_7", Want: "gen_feat_min_group_order_desc_7", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
- g.Entry("gen: count group order desc", helpers.Args{Input: "gen_feat_count_group_order_desc_8", Want: "gen_feat_count_group_order_desc_8", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
- g.Entry("gen: sum group order desc", helpers.Args{Input: "gen_feat_sum_group_order_desc_9", Want: "gen_feat_sum_group_order_desc_9", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
+ g.Entry("gen: mean top desc group order asc",
+ helpers.Args{
+ Input: "gen_feat_mean_top_desc_group_order_asc_0", Want: "gen_feat_mean_top_desc_group_order_asc_0",
+ Duration: 25 * time.Minute, Offset: -20 * time.Minute,
+ }),
+ g.Entry("gen: mean top asc group order desc filter",
+ helpers.Args{
+ Input: "gen_feat_mean_top_asc_group_order_desc_filter_1", Want: "gen_feat_mean_top_asc_group_order_desc_filter_1",
+ Duration: 25 * time.Minute, Offset: -20 * time.Minute,
+ }),
+ g.Entry("gen: mean group",
+ helpers.Args{Input: "gen_feat_mean_group_2", Want: "gen_feat_mean_group_2", DisOrder: true, Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
+ g.Entry("gen: mean top desc group order desc",
+ helpers.Args{
+ Input: "gen_feat_mean_top_desc_group_order_desc_3", Want: "gen_feat_mean_top_desc_group_order_desc_3",
+ Duration: 25 * time.Minute, Offset: -20 * time.Minute,
+ }),
+ g.Entry("gen: mean top asc group order asc",
+ helpers.Args{Input: "gen_feat_mean_top_asc_group_order_asc_4", Want: "gen_feat_mean_top_asc_group_order_asc_4", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
+ g.Entry("gen: mean group order asc",
+ helpers.Args{Input: "gen_feat_mean_group_order_asc_5", Want: "gen_feat_mean_group_order_asc_5", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
+ g.Entry("gen: max group order desc",
+ helpers.Args{Input: "gen_feat_max_group_order_desc_6", Want: "gen_feat_max_group_order_desc_6", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
+ g.Entry("gen: min group order desc",
+ helpers.Args{Input: "gen_feat_min_group_order_desc_7", Want: "gen_feat_min_group_order_desc_7", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
+ g.Entry("gen: count group order desc",
+ helpers.Args{Input: "gen_feat_count_group_order_desc_8", Want: "gen_feat_count_group_order_desc_8", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
+ g.Entry("gen: sum group order desc",
+ helpers.Args{Input: "gen_feat_sum_group_order_desc_9", Want: "gen_feat_sum_group_order_desc_9", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
}
// RegisterTable registers the measure test table with the given description.