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.