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

hanahmily pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git


The following commit(s) were added to refs/heads/main by this push:
     new f8cb0194 Remove check requiring tags in criteria to be present in 
projection (#876)
f8cb0194 is described below

commit f8cb019441e09a41db5dfda6af529a96cf3130de
Author: Gao Hongtao <[email protected]>
AuthorDate: Tue Dec 2 12:37:21 2025 +0800

    Remove check requiring tags in criteria to be present in projection (#876)
    
    
    
    * Refactor query engine to eliminate unnecessary validation for criteria 
tags in projections.
---
 CHANGES.md                                         |   1 +
 banyand/liaison/grpc/measure.go                    |   2 +-
 banyand/liaison/grpc/stream.go                     |   2 +-
 banyand/liaison/grpc/trace.go                      |   2 +-
 pkg/query/logical/criteria_tags.go                 |  42 +++++
 pkg/query/logical/hidden_tags.go                   | 158 +++++++++++++++++
 pkg/query/logical/hidden_tags_test.go              | 186 ++++++++++++++++++++
 .../measure/measure_plan_indexscan_local.go        |  64 ++++++-
 .../measure/measure_plan_indexscan_local_test.go   | 163 ++++++++++++++++++
 pkg/query/logical/stream/stream_plan_tag_filter.go | 189 ++++++++++++++++++---
 .../logical/stream/stream_plan_tag_filter_test.go  | 152 +++++++++++++++++
 test/cases/measure/data/input/filter_hidden_tag.ql |  22 +++
 .../measure/data/input/filter_hidden_tag.yaml      |  35 ++++
 .../data/input/index_mode_filter_hidden_tag.ql     |  22 +++
 .../data/input/index_mode_filter_hidden_tag.yaml   |  33 ++++
 .../cases/measure/data/want/filter_hidden_tag.yaml | 126 ++++++++++++++
 .../data/want/index_mode_filter_hidden_tag.yaml    |  38 +++++
 test/cases/measure/measure.go                      |   2 +
 test/cases/stream/data/input/filter_hidden_tag.ql  |  21 +++
 .../cases/stream/data/input/filter_hidden_tag.yaml |  41 +++++
 test/cases/stream/data/want/filter_hidden_tag.yaml |  43 +++++
 test/cases/stream/stream.go                        |   1 +
 22 files changed, 1310 insertions(+), 35 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 9ce68fdd..32eb43ac 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -8,6 +8,7 @@ Release Notes.
 
 - Remove Bloom filter for dictionary-encoded tags.
 - Implement BanyanDB MCP.
+- Remove check requiring tags in criteria to be present in projection
 
 ### Bug Fixes
 
diff --git a/banyand/liaison/grpc/measure.go b/banyand/liaison/grpc/measure.go
index 17291452..355968d0 100644
--- a/banyand/liaison/grpc/measure.go
+++ b/banyand/liaison/grpc/measure.go
@@ -95,7 +95,7 @@ func (ms *measureService) Write(measure 
measurev1.MeasureService_WriteServer) er
                        return nil
                }
                if err != nil {
-                       if !errors.Is(err, context.DeadlineExceeded) && 
!errors.Is(err, context.Canceled) {
+                       if status.Code(err) != codes.Canceled && 
status.Code(err) != codes.DeadlineExceeded {
                                ms.l.Error().Err(err).Stringer("written", 
writeRequest).Msg("failed to receive message")
                        }
                        return err
diff --git a/banyand/liaison/grpc/stream.go b/banyand/liaison/grpc/stream.go
index d0da8c6f..4b31874b 100644
--- a/banyand/liaison/grpc/stream.go
+++ b/banyand/liaison/grpc/stream.go
@@ -194,7 +194,7 @@ func (s *streamService) Write(stream 
streamv1.StreamService_WriteServer) error {
                        return nil
                }
                if err != nil {
-                       if !errors.Is(err, context.DeadlineExceeded) && 
!errors.Is(err, context.Canceled) {
+                       if status.Code(err) != codes.Canceled && 
status.Code(err) != codes.DeadlineExceeded {
                                s.l.Error().Stringer("written", 
writeEntity).Err(err).Msg("failed to receive message")
                        }
                        return err
diff --git a/banyand/liaison/grpc/trace.go b/banyand/liaison/grpc/trace.go
index c4989652..89125a21 100644
--- a/banyand/liaison/grpc/trace.go
+++ b/banyand/liaison/grpc/trace.go
@@ -257,7 +257,7 @@ func (s *traceService) Write(stream 
tracev1.TraceService_WriteServer) error {
                        return nil
                }
                if err != nil {
-                       if !errors.Is(err, context.DeadlineExceeded) && 
!errors.Is(err, context.Canceled) {
+                       if status.Code(err) != codes.Canceled && 
status.Code(err) != codes.DeadlineExceeded {
                                s.l.Error().Stringer("written", 
writeEntity).Err(err).Msg("failed to receive message")
                        }
                        return err
diff --git a/pkg/query/logical/criteria_tags.go 
b/pkg/query/logical/criteria_tags.go
new file mode 100644
index 00000000..2712df1a
--- /dev/null
+++ b/pkg/query/logical/criteria_tags.go
@@ -0,0 +1,42 @@
+// 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.
+
+package logical
+
+import modelv1 
"github.com/apache/skywalking-banyandb/api/proto/banyandb/model/v1"
+
+// CollectCriteriaTagNames walks through the provided criteria expression and
+// records every referenced tag name into the supplied map. The map acts as a 
set
+// that deduplicates tag names for callers that need to know all tags used in a
+// criteria tree.
+func CollectCriteriaTagNames(criteria *modelv1.Criteria, tags 
map[string]struct{}) {
+       if criteria == nil || tags == nil {
+               return
+       }
+       switch exp := criteria.GetExp().(type) {
+       case *modelv1.Criteria_Condition:
+               if cond := criteria.GetCondition(); cond != nil {
+                       tags[cond.GetName()] = struct{}{}
+               }
+       case *modelv1.Criteria_Le:
+               if exp.Le == nil {
+                       return
+               }
+               CollectCriteriaTagNames(exp.Le.Left, tags)
+               CollectCriteriaTagNames(exp.Le.Right, tags)
+       }
+}
diff --git a/pkg/query/logical/hidden_tags.go b/pkg/query/logical/hidden_tags.go
new file mode 100644
index 00000000..bd49bcbe
--- /dev/null
+++ b/pkg/query/logical/hidden_tags.go
@@ -0,0 +1,158 @@
+// 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.
+
+package logical
+
+import (
+       modelv1 
"github.com/apache/skywalking-banyandb/api/proto/banyandb/model/v1"
+)
+
+// HiddenTagSet is a set of tag names that should be stripped from query 
results.
+// These are tags that were used in criteria but not requested in the 
projection.
+type HiddenTagSet map[string]struct{}
+
+// NewHiddenTagSet creates a new HiddenTagSet.
+func NewHiddenTagSet() HiddenTagSet {
+       return make(map[string]struct{})
+}
+
+// Add adds a tag name to the hidden set.
+func (h HiddenTagSet) Add(tagName string) {
+       h[tagName] = struct{}{}
+}
+
+// Contains checks if a tag name is in the hidden set.
+func (h HiddenTagSet) Contains(tagName string) bool {
+       _, ok := h[tagName]
+       return ok
+}
+
+// IsEmpty returns true if the hidden set has no entries.
+func (h HiddenTagSet) IsEmpty() bool {
+       return len(h) == 0
+}
+
+// StripHiddenTags removes hidden tags from a slice of tag families.
+// It modifies the slice in place and returns the filtered result.
+// Empty tag families are removed from the result.
+func (h HiddenTagSet) StripHiddenTags(tagFamilies []*modelv1.TagFamily) 
[]*modelv1.TagFamily {
+       if h.IsEmpty() || len(tagFamilies) == 0 {
+               return tagFamilies
+       }
+
+       families := tagFamilies[:0]
+       for _, tf := range tagFamilies {
+               if tf == nil {
+                       continue
+               }
+               tags := tf.Tags[:0]
+               for _, tag := range tf.Tags {
+                       if tag == nil {
+                               continue
+                       }
+                       if h.Contains(tag.GetKey()) {
+                               continue
+                       }
+                       tags = append(tags, tag)
+               }
+               if len(tags) == 0 {
+                       continue
+               }
+               tf.Tags = tags
+               families = append(families, tf)
+       }
+       return families
+}
+
+// CollectHiddenCriteriaTags identifies tags used in criteria that are not in 
the projection.
+// It returns a HiddenTagSet containing those tag names and an optional slice 
of Tag
+// structures grouped by family for adding to the projection.
+//
+// Parameters:
+//   - criteria: The query criteria containing filter conditions
+//   - projectedTagNames: Set of tag names already in the projection
+//   - entityDict: Map of entity tag names (these are skipped)
+//   - schema: The schema to look up tag family information
+//   - tagFamilyGetter: Function to get tag families from the schema
+//
+// Returns:
+//   - HiddenTagSet: Set of hidden tag names
+//   - [][]*Tag: Tags grouped by family to add to projection (nil if empty)
+func CollectHiddenCriteriaTags(
+       criteria *modelv1.Criteria,
+       projectedTagNames map[string]struct{},
+       entityDict map[string]int,
+       schema Schema,
+       tagFamilyGetter func() []string,
+) (HiddenTagSet, [][]*Tag) {
+       hidden := NewHiddenTagSet()
+       if criteria == nil {
+               return hidden, nil
+       }
+
+       // Collect all tag names from criteria
+       tagNames := make(map[string]struct{})
+       CollectCriteriaTagNames(criteria, tagNames)
+
+       // Group tags by family
+       familyTags := make(map[string][]*Tag)
+       var familyOrder []string
+
+       tagFamilies := tagFamilyGetter()
+
+       for tagName := range tagNames {
+               // Skip if already in projection
+               if _, ok := projectedTagNames[tagName]; ok {
+                       continue
+               }
+               // Skip entity tags
+               if _, isEntity := entityDict[tagName]; isEntity {
+                       continue
+               }
+
+               tagSpec := schema.FindTagSpecByName(tagName)
+               if tagSpec == nil {
+                       continue
+               }
+
+               // Get the actual family name from the schema
+               if tagSpec.TagFamilyIdx < 0 || tagSpec.TagFamilyIdx >= 
len(tagFamilies) {
+                       continue
+               }
+               familyName := tagFamilies[tagSpec.TagFamilyIdx]
+
+               // Mark as hidden
+               hidden.Add(tagName)
+
+               // Group tags by family
+               if _, exists := familyTags[familyName]; !exists {
+                       familyOrder = append(familyOrder, familyName)
+                       familyTags[familyName] = make([]*Tag, 0)
+               }
+               familyTags[familyName] = append(familyTags[familyName], 
NewTag(familyName, tagName))
+       }
+
+       // Convert to [][]*Tag maintaining family grouping
+       if len(familyOrder) == 0 {
+               return hidden, nil
+       }
+       extras := make([][]*Tag, 0, len(familyOrder))
+       for _, family := range familyOrder {
+               extras = append(extras, familyTags[family])
+       }
+       return hidden, extras
+}
diff --git a/pkg/query/logical/hidden_tags_test.go 
b/pkg/query/logical/hidden_tags_test.go
new file mode 100644
index 00000000..82881f8d
--- /dev/null
+++ b/pkg/query/logical/hidden_tags_test.go
@@ -0,0 +1,186 @@
+// 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.
+
+package logical
+
+import (
+       "testing"
+
+       modelv1 
"github.com/apache/skywalking-banyandb/api/proto/banyandb/model/v1"
+)
+
+func TestHiddenTagSet(t *testing.T) {
+       t.Run("NewHiddenTagSet creates empty set", func(t *testing.T) {
+               h := NewHiddenTagSet()
+               if !h.IsEmpty() {
+                       t.Error("expected new HiddenTagSet to be empty")
+               }
+       })
+
+       t.Run("Add and Contains", func(t *testing.T) {
+               h := NewHiddenTagSet()
+               h.Add("tag1")
+               h.Add("tag2")
+
+               if !h.Contains("tag1") {
+                       t.Error("expected tag1 to be in set")
+               }
+               if !h.Contains("tag2") {
+                       t.Error("expected tag2 to be in set")
+               }
+               if h.Contains("tag3") {
+                       t.Error("expected tag3 not to be in set")
+               }
+               if h.IsEmpty() {
+                       t.Error("expected set not to be empty after adding 
tags")
+               }
+       })
+
+       t.Run("StripHiddenTags removes hidden tags", func(t *testing.T) {
+               h := NewHiddenTagSet()
+               h.Add("hidden1")
+               h.Add("hidden2")
+
+               families := []*modelv1.TagFamily{
+                       {
+                               Name: "family1",
+                               Tags: []*modelv1.Tag{
+                                       {Key: "visible", Value: 
&modelv1.TagValue{Value: &modelv1.TagValue_Str{Str: &modelv1.Str{Value: 
"keep"}}}},
+                                       {Key: "hidden1", Value: 
&modelv1.TagValue{Value: &modelv1.TagValue_Str{Str: &modelv1.Str{Value: 
"drop"}}}},
+                               },
+                       },
+                       {
+                               Name: "family2",
+                               Tags: []*modelv1.Tag{
+                                       {Key: "hidden2", Value: 
&modelv1.TagValue{Value: &modelv1.TagValue_Str{Str: &modelv1.Str{Value: 
"drop"}}}},
+                               },
+                       },
+                       {
+                               Name: "family3",
+                               Tags: []*modelv1.Tag{
+                                       {Key: "also_visible", Value: 
&modelv1.TagValue{Value: &modelv1.TagValue_Str{Str: &modelv1.Str{Value: 
"keep"}}}},
+                               },
+                       },
+               }
+
+               result := h.StripHiddenTags(families)
+
+               // family2 should be removed entirely because all tags are 
hidden
+               if len(result) != 2 {
+                       t.Fatalf("expected 2 families after stripping, got %d", 
len(result))
+               }
+
+               // Check family1
+               if result[0].Name != "family1" {
+                       t.Errorf("expected first family to be 'family1', got 
%s", result[0].Name)
+               }
+               if len(result[0].Tags) != 1 {
+                       t.Fatalf("expected 1 tag in family1, got %d", 
len(result[0].Tags))
+               }
+               if result[0].Tags[0].Key != "visible" {
+                       t.Errorf("expected tag 'visible' in family1, got %s", 
result[0].Tags[0].Key)
+               }
+
+               // Check family3
+               if result[1].Name != "family3" {
+                       t.Errorf("expected second family to be 'family3', got 
%s", result[1].Name)
+               }
+               if len(result[1].Tags) != 1 {
+                       t.Fatalf("expected 1 tag in family3, got %d", 
len(result[1].Tags))
+               }
+               if result[1].Tags[0].Key != "also_visible" {
+                       t.Errorf("expected tag 'also_visible' in family3, got 
%s", result[1].Tags[0].Key)
+               }
+       })
+
+       t.Run("StripHiddenTags with empty hidden set returns unchanged", func(t 
*testing.T) {
+               h := NewHiddenTagSet()
+               families := []*modelv1.TagFamily{
+                       {
+                               Name: "family1",
+                               Tags: []*modelv1.Tag{
+                                       {Key: "tag1", Value: 
&modelv1.TagValue{Value: &modelv1.TagValue_Str{Str: &modelv1.Str{Value: 
"value"}}}},
+                               },
+                       },
+               }
+
+               result := h.StripHiddenTags(families)
+
+               if len(result) != 1 {
+                       t.Fatalf("expected 1 family, got %d", len(result))
+               }
+               if len(result[0].Tags) != 1 {
+                       t.Fatalf("expected 1 tag, got %d", len(result[0].Tags))
+               }
+       })
+
+       t.Run("StripHiddenTags with nil families returns nil", func(t 
*testing.T) {
+               h := NewHiddenTagSet()
+               h.Add("hidden")
+
+               result := h.StripHiddenTags(nil)
+
+               if result != nil {
+                       t.Errorf("expected nil result for nil input, got %v", 
result)
+               }
+       })
+
+       t.Run("StripHiddenTags handles nil tag families", func(t *testing.T) {
+               h := NewHiddenTagSet()
+               h.Add("hidden")
+
+               families := []*modelv1.TagFamily{
+                       nil,
+                       {
+                               Name: "family1",
+                               Tags: []*modelv1.Tag{
+                                       {Key: "visible", Value: 
&modelv1.TagValue{Value: &modelv1.TagValue_Str{Str: &modelv1.Str{Value: 
"keep"}}}},
+                               },
+                       },
+               }
+
+               result := h.StripHiddenTags(families)
+
+               if len(result) != 1 {
+                       t.Fatalf("expected 1 family after stripping nil, got 
%d", len(result))
+               }
+       })
+
+       t.Run("StripHiddenTags handles nil tags", func(t *testing.T) {
+               h := NewHiddenTagSet()
+               h.Add("hidden")
+
+               families := []*modelv1.TagFamily{
+                       {
+                               Name: "family1",
+                               Tags: []*modelv1.Tag{
+                                       nil,
+                                       {Key: "visible", Value: 
&modelv1.TagValue{Value: &modelv1.TagValue_Str{Str: &modelv1.Str{Value: 
"keep"}}}},
+                               },
+                       },
+               }
+
+               result := h.StripHiddenTags(families)
+
+               if len(result) != 1 {
+                       t.Fatalf("expected 1 family, got %d", len(result))
+               }
+               if len(result[0].Tags) != 1 {
+                       t.Fatalf("expected 1 tag after stripping nil, got %d", 
len(result[0].Tags))
+               }
+       })
+}
diff --git a/pkg/query/logical/measure/measure_plan_indexscan_local.go 
b/pkg/query/logical/measure/measure_plan_indexscan_local.go
index c5fc25ac..08c57449 100644
--- a/pkg/query/logical/measure/measure_plan_indexscan_local.go
+++ b/pkg/query/logical/measure/measure_plan_indexscan_local.go
@@ -53,16 +53,24 @@ type unresolvedIndexScan struct {
 
 func (uis *unresolvedIndexScan) Analyze(s logical.Schema) (logical.Plan, 
error) {
        projTags := make([]model.TagProjection, len(uis.projectionTags))
+       projectedTagNames := make(map[string]struct{})
        var projTagsRefs [][]*logical.TagRef
        if len(uis.projectionTags) > 0 {
                for i := range uis.projectionTags {
                        for _, tag := range uis.projectionTags[i] {
                                projTags[i].Family = tag.GetFamilyName()
                                projTags[i].Names = append(projTags[i].Names, 
tag.GetTagName())
+                               projectedTagNames[tag.GetTagName()] = struct{}{}
                        }
                }
+       }
+       tagRefInput := make([][]*logical.Tag, 0, len(uis.projectionTags))
+       tagRefInput = append(tagRefInput, uis.projectionTags...)
+
+       // Create tag refs for projection tags before any early returns
+       if len(tagRefInput) > 0 {
                var err error
-               projTagsRefs, err = s.CreateTagRef(uis.projectionTags...)
+               projTagsRefs, err = s.CreateTagRef(tagRefInput...)
                if err != nil {
                        return nil, err
                }
@@ -112,6 +120,31 @@ func (uis *unresolvedIndexScan) Analyze(s logical.Schema) 
(logical.Plan, error)
                // fill AnyEntry by default
                entity[idx] = pbv1.AnyTagValue
        }
+
+       // Collect hidden criteria tags using shared utility
+       tagFamilies := ms.measure.GetTagFamilies()
+       hiddenTags, extraTags := logical.CollectHiddenCriteriaTags(
+               uis.criteria,
+               projectedTagNames,
+               entityMap,
+               s,
+               func() []string {
+                       names := make([]string, len(tagFamilies))
+                       for i, tf := range tagFamilies {
+                               names[i] = tf.GetName()
+                       }
+                       return names
+               },
+       )
+       if len(extraTags) > 0 {
+               tagRefInput = append(tagRefInput, extraTags...)
+               // Recreate tag refs with both projection tags and hidden 
criteria tags
+               var err error
+               projTagsRefs, err = s.CreateTagRef(tagRefInput...)
+               if err != nil {
+                       return nil, err
+               }
+       }
        query, entities, _, err := inverted.BuildQuery(uis.criteria, s, 
entityMap, entity)
        if err != nil {
                return nil, err
@@ -128,6 +161,7 @@ func (uis *unresolvedIndexScan) Analyze(s logical.Schema) 
(logical.Plan, error)
                query:                query,
                entities:             entities,
                groupByEntity:        uis.groupByEntity,
+               hiddenTags:           hiddenTags,
                uis:                  uis,
                l:                    logger.GetLogger("query", "measure", 
uis.metadata.Group, uis.metadata.Name, "local-index"),
                ec:                   uis.ec,
@@ -147,6 +181,7 @@ type localIndexScan struct {
        order                *logical.OrderBy
        metadata             *commonv1.Metadata
        l                    *logger.Logger
+       hiddenTags           logical.HiddenTagSet
        timeRange            timestamp.TimeRange
        projectionTagsRefs   [][]*logical.TagRef
        projectionFieldsRefs []*logical.FieldRef
@@ -195,7 +230,8 @@ func (i *localIndexScan) Execute(ctx context.Context) (mit 
executor.MIterator, e
                return nil, fmt.Errorf("failed to query measure: %w", err)
        }
        return &resultMIterator{
-               result: result,
+               result:     result,
+               hiddenTags: i.hiddenTags,
        }, nil
 }
 
@@ -210,10 +246,16 @@ func (i *localIndexScan) Children() []logical.Plan {
 }
 
 func (i *localIndexScan) Schema() logical.Schema {
-       if len(i.projectionTagsRefs) == 0 {
-               return i.schema
+       schema := i.schema
+       if len(i.projectionTagsRefs) > 0 {
+               if projected := schema.ProjTags(i.projectionTagsRefs...); 
projected != nil {
+                       schema = projected
+               }
+       }
+       if len(i.projectionFieldsRefs) > 0 {
+               schema = schema.ProjFields(i.projectionFieldsRefs...)
        }
-       return 
i.schema.ProjTags(i.projectionTagsRefs...).ProjFields(i.projectionFieldsRefs...)
+       return schema
 }
 
 func indexScan(startTime, endTime time.Time, metadata *commonv1.Metadata, 
projectionTags [][]*logical.Tag,
@@ -232,10 +274,11 @@ func indexScan(startTime, endTime time.Time, metadata 
*commonv1.Metadata, projec
 }
 
 type resultMIterator struct {
-       result  model.MeasureQueryResult
-       err     error
-       current []*measurev1.DataPoint
-       i       int
+       result     model.MeasureQueryResult
+       hiddenTags logical.HiddenTagSet
+       err        error
+       current    []*measurev1.DataPoint
+       i          int
 }
 
 func (ei *resultMIterator) Next() bool {
@@ -276,6 +319,9 @@ func (ei *resultMIterator) Next() bool {
                                })
                        }
                }
+               // Strip hidden tags from the result
+               dp.TagFamilies = ei.hiddenTags.StripHiddenTags(dp.TagFamilies)
+
                for _, f := range r.Fields {
                        dp.Fields = append(dp.Fields, 
&measurev1.DataPoint_Field{
                                Name:  f.Name,
diff --git a/pkg/query/logical/measure/measure_plan_indexscan_local_test.go 
b/pkg/query/logical/measure/measure_plan_indexscan_local_test.go
new file mode 100644
index 00000000..d83a8bf4
--- /dev/null
+++ b/pkg/query/logical/measure/measure_plan_indexscan_local_test.go
@@ -0,0 +1,163 @@
+// 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.
+
+package measure
+
+import (
+       "testing"
+       "time"
+
+       commonv1 
"github.com/apache/skywalking-banyandb/api/proto/banyandb/common/v1"
+       databasev1 
"github.com/apache/skywalking-banyandb/api/proto/banyandb/database/v1"
+       modelv1 
"github.com/apache/skywalking-banyandb/api/proto/banyandb/model/v1"
+       "github.com/apache/skywalking-banyandb/pkg/query/logical"
+)
+
+func TestLocalIndexScanSchemaRetainsNonProjectedTags(t *testing.T) {
+       measureMeta := &databasev1.Measure{
+               Entity: &databasev1.Entity{
+                       TagNames: []string{"entity_id"},
+               },
+               TagFamilies: []*databasev1.TagFamilySpec{
+                       {
+                               Name: "default",
+                               Tags: []*databasev1.TagSpec{
+                                       {Name: "filter_tag", Type: 
databasev1.TagType_TAG_TYPE_STRING},
+                                       {Name: "projected_tag", Type: 
databasev1.TagType_TAG_TYPE_STRING},
+                               },
+                       },
+               },
+       }
+       indexRules := []*databasev1.IndexRule{
+               {
+                       Metadata: &commonv1.Metadata{Name: "filter-tag-rule"},
+                       Type:     databasev1.IndexRule_TYPE_INVERTED,
+                       Tags:     []string{"filter_tag"},
+               },
+       }
+       schema, err := BuildSchema(measureMeta, indexRules)
+       if err != nil {
+               t.Fatalf("build schema: %v", err)
+       }
+
+       criteria := &modelv1.Criteria{
+               Exp: &modelv1.Criteria_Condition{
+                       Condition: &modelv1.Condition{
+                               Name: "filter_tag",
+                               Op:   modelv1.Condition_BINARY_OP_EQ,
+                               Value: &modelv1.TagValue{
+                                       Value: &modelv1.TagValue_Str{
+                                               Str: &modelv1.Str{Value: 
"match"},
+                                       },
+                               },
+                       },
+               },
+       }
+       metadata := &commonv1.Metadata{Name: "test", Group: "default"}
+
+       plan, err := indexScan(
+               time.Unix(0, 0),
+               time.Unix(1, 0),
+               metadata,
+               [][]*logical.Tag{logical.NewTags("default", "projected_tag")},
+               nil,
+               false,
+               criteria,
+               nil,
+       ).Analyze(schema)
+       if err != nil {
+               t.Fatalf("analyze plan: %v", err)
+       }
+
+       if plan.Schema().FindTagSpecByName("filter_tag") == nil {
+               t.Fatalf("expected schema to retain definition for filter_tag 
even when it is not projected")
+       }
+}
+
+func TestLocalIndexScanSchemaProjectionInIndexMode(t *testing.T) {
+       measureMeta := &databasev1.Measure{
+               Entity: &databasev1.Entity{
+                       TagNames: []string{"entity_id"},
+               },
+               TagFamilies: []*databasev1.TagFamilySpec{
+                       {
+                               Name: "default",
+                               Tags: []*databasev1.TagSpec{
+                                       {Name: "entity_id", Type: 
databasev1.TagType_TAG_TYPE_STRING},
+                                       {Name: "filter_tag", Type: 
databasev1.TagType_TAG_TYPE_STRING},
+                                       {Name: "projected_tag", Type: 
databasev1.TagType_TAG_TYPE_STRING},
+                                       {Name: "non_projected_tag", Type: 
databasev1.TagType_TAG_TYPE_STRING},
+                               },
+                       },
+               },
+               IndexMode: true,
+       }
+       indexRules := []*databasev1.IndexRule{
+               {
+                       Metadata: &commonv1.Metadata{Name: "filter-tag-rule"},
+                       Type:     databasev1.IndexRule_TYPE_INVERTED,
+                       Tags:     []string{"filter_tag"},
+               },
+       }
+       schema, err := BuildSchema(measureMeta, indexRules)
+       if err != nil {
+               t.Fatalf("build schema: %v", err)
+       }
+
+       criteria := &modelv1.Criteria{
+               Exp: &modelv1.Criteria_Condition{
+                       Condition: &modelv1.Condition{
+                               Name: "filter_tag",
+                               Op:   modelv1.Condition_BINARY_OP_EQ,
+                               Value: &modelv1.TagValue{
+                                       Value: &modelv1.TagValue_Str{
+                                               Str: &modelv1.Str{Value: 
"match"},
+                                       },
+                               },
+                       },
+               },
+       }
+       metadata := &commonv1.Metadata{Name: "test", Group: "default"}
+
+       plan, err := indexScan(
+               time.Unix(0, 0),
+               time.Unix(1, 0),
+               metadata,
+               [][]*logical.Tag{logical.NewTags("default", "projected_tag")},
+               nil,
+               false,
+               criteria,
+               nil,
+       ).Analyze(schema)
+       if err != nil {
+               t.Fatalf("analyze plan: %v", err)
+       }
+
+       projectedSchema := plan.Schema()
+
+       // Verify that projected_tag is present in the schema
+       if projectedSchema.FindTagSpecByName("projected_tag") == nil {
+               t.Fatalf("expected schema to include projected_tag")
+       }
+
+       // Verify that non_projected_tag is NOT present in the projected schema
+       // This is the key test - without the fix, projectedSchema would be the 
full schema
+       // and non_projected_tag would incorrectly be present
+       if projectedSchema.FindTagSpecByName("non_projected_tag") != nil {
+               t.Fatalf("expected schema to exclude non_projected_tag when 
using projection")
+       }
+}
diff --git a/pkg/query/logical/stream/stream_plan_tag_filter.go 
b/pkg/query/logical/stream/stream_plan_tag_filter.go
index a80e4b6c..ee0533bf 100644
--- a/pkg/query/logical/stream/stream_plan_tag_filter.go
+++ b/pkg/query/logical/stream/stream_plan_tag_filter.go
@@ -66,31 +66,52 @@ func (uis *unresolvedTagFilter) Analyze(s logical.Schema) 
(logical.Plan, error)
                return nil, err
        }
 
-       projTags := make([]model.TagProjection, len(uis.projectionTags))
-       if len(uis.projectionTags) > 0 {
-               for i := range uis.projectionTags {
-                       for _, tag := range uis.projectionTags[i] {
-                               projTags[i].Family = tag.GetFamilyName()
-                               projTags[i].Names = append(projTags[i].Names, 
tag.GetTagName())
+       projBuilder := newProjectionBuilder(uis.projectionTags)
+       criteriaTagNames := make(map[string]struct{})
+       logical.CollectCriteriaTagNames(uis.criteria, criteriaTagNames)
+
+       hiddenTags := logical.NewHiddenTagSet()
+       var tagFilter logical.TagFilter
+       if uis.criteria != nil {
+               var errFilter error
+               tagFilter, errFilter = logical.BuildTagFilter(uis.criteria, 
entityDict, s, s, false, "")
+               if errFilter != nil {
+                       return nil, errFilter
+               }
+               if tagFilter != logical.DummyFilter {
+                       for tagName := range criteriaTagNames {
+                               if _, isEntity := entityDict[tagName]; isEntity 
{
+                                       continue
+                               }
+                               added, errAdd := 
projBuilder.AddTagFromSchema(s, tagName)
+                               if errAdd != nil {
+                                       return nil, errAdd
+                               }
+                               if added {
+                                       hiddenTags.Add(tagName)
+                               }
                        }
                }
+       }
+
+       ctx.projectionTags = projBuilder.Model()
+       if logicalTags := projBuilder.LogicalTags(); len(logicalTags) > 0 {
                var errProject error
-               ctx.projTagsRefs, errProject = 
s.CreateTagRef(uis.projectionTags...)
+               ctx.projTagsRefs, errProject = s.CreateTagRef(logicalTags...)
                if errProject != nil {
                        return nil, errProject
                }
        }
-       ctx.projectionTags = projTags
+
        plan := uis.selectIndexScanner(ctx, uis.ec)
-       if uis.criteria != nil {
-               tagFilter, errFilter := logical.BuildTagFilter(uis.criteria, 
entityDict, s, s, false, "")
-               if errFilter != nil {
-                       return nil, errFilter
-               }
-               if tagFilter != logical.DummyFilter {
-                       // create tagFilter with a projected view
-                       plan = newTagFilter(s.ProjTags(ctx.projTagsRefs...), 
plan, tagFilter)
+       if uis.criteria != nil && tagFilter != nil && tagFilter != 
logical.DummyFilter {
+               projSchema := s
+               if len(ctx.projTagsRefs) > 0 {
+                       if projected := s.ProjTags(ctx.projTagsRefs...); 
projected != nil {
+                               projSchema = projected
+                       }
                }
+               plan = newTagFilter(projSchema, plan, tagFilter, hiddenTags)
        }
        return plan, err
 }
@@ -144,20 +165,22 @@ var (
 )
 
 type tagFilterPlan struct {
-       s         logical.Schema
-       parent    logical.Plan
-       tagFilter logical.TagFilter
+       s          logical.Schema
+       parent     logical.Plan
+       tagFilter  logical.TagFilter
+       hiddenTags logical.HiddenTagSet
 }
 
 func (t *tagFilterPlan) Close() {
        t.parent.(executor.StreamExecutable).Close()
 }
 
-func newTagFilter(s logical.Schema, parent logical.Plan, tagFilter 
logical.TagFilter) logical.Plan {
+func newTagFilter(s logical.Schema, parent logical.Plan, tagFilter 
logical.TagFilter, hiddenTags logical.HiddenTagSet) logical.Plan {
        return &tagFilterPlan{
-               s:         s,
-               parent:    parent,
-               tagFilter: tagFilter,
+               s:          s,
+               parent:     parent,
+               tagFilter:  tagFilter,
+               hiddenTags: hiddenTags,
        }
 }
 
@@ -178,6 +201,8 @@ func (t *tagFilterPlan) Execute(ec context.Context) 
([]*streamv1.Element, error)
                                return nil, err
                        }
                        if ok {
+                               // Strip hidden tags using shared utility
+                               e.TagFamilies = 
t.hiddenTags.StripHiddenTags(e.TagFamilies)
                                filteredElements = append(filteredElements, e)
                        }
                }
@@ -200,3 +225,121 @@ func (t *tagFilterPlan) Children() []logical.Plan {
 func (t *tagFilterPlan) Schema() logical.Schema {
        return t.s
 }
+
+type projectionBuilder struct {
+       familyTags  map[string][]string
+       tagSet      map[string]struct{}
+       familyOrder []string
+}
+
+func newProjectionBuilder(existing [][]*logical.Tag) *projectionBuilder {
+       pb := &projectionBuilder{
+               familyTags: make(map[string][]string),
+               tagSet:     make(map[string]struct{}),
+       }
+       for _, tags := range existing {
+               if len(tags) == 0 {
+                       continue
+               }
+               family := tags[0].GetFamilyName()
+               pb.ensureFamily(family)
+               for _, tag := range tags {
+                       pb.addTag(family, tag.GetTagName())
+               }
+       }
+       return pb
+}
+
+func (pb *projectionBuilder) HasTag(tagName string) bool {
+       _, ok := pb.tagSet[tagName]
+       return ok
+}
+
+func (pb *projectionBuilder) addTag(family, tagName string) {
+       if tagName == "" || pb.HasTag(tagName) {
+               return
+       }
+       pb.ensureFamily(family)
+       pb.familyTags[family] = append(pb.familyTags[family], tagName)
+       pb.tagSet[tagName] = struct{}{}
+}
+
+func (pb *projectionBuilder) ensureFamily(family string) {
+       if _, ok := pb.familyTags[family]; ok {
+               return
+       }
+       pb.familyOrder = append(pb.familyOrder, family)
+       pb.familyTags[family] = make([]string, 0)
+}
+
+func (pb *projectionBuilder) AddTagFromSchema(s logical.Schema, tagName 
string) (bool, error) {
+       if pb.HasTag(tagName) {
+               return false, nil
+       }
+       tagSpec := s.FindTagSpecByName(tagName)
+       if tagSpec == nil {
+               return false, fmt.Errorf("tag %s not defined in schema", 
tagName)
+       }
+       family, ok := familyNameFromSchema(s, tagSpec)
+       if !ok {
+               return false, fmt.Errorf("tag family not found for %s", tagName)
+       }
+       pb.addTag(family, tagName)
+       return true, nil
+}
+
+func (pb *projectionBuilder) Model() []model.TagProjection {
+       if len(pb.familyOrder) == 0 {
+               return nil
+       }
+       projections := make([]model.TagProjection, 0, len(pb.familyOrder))
+       for _, family := range pb.familyOrder {
+               names := pb.familyTags[family]
+               if len(names) == 0 {
+                       continue
+               }
+               proj := model.TagProjection{
+                       Family: family,
+                       Names:  append([]string(nil), names...),
+               }
+               projections = append(projections, proj)
+       }
+       if len(projections) == 0 {
+               return nil
+       }
+       return projections
+}
+
+func (pb *projectionBuilder) LogicalTags() [][]*logical.Tag {
+       if len(pb.familyOrder) == 0 {
+               return nil
+       }
+       logicalTags := make([][]*logical.Tag, 0, len(pb.familyOrder))
+       for _, family := range pb.familyOrder {
+               names := pb.familyTags[family]
+               if len(names) == 0 {
+                       continue
+               }
+               familyTags := make([]*logical.Tag, 0, len(names))
+               for _, name := range names {
+                       familyTags = append(familyTags, logical.NewTag(family, 
name))
+               }
+               logicalTags = append(logicalTags, familyTags)
+       }
+       if len(logicalTags) == 0 {
+               return nil
+       }
+       return logicalTags
+}
+
+func familyNameFromSchema(s logical.Schema, tagSpec *logical.TagSpec) (string, 
bool) {
+       streamSchema, ok := s.(*schema)
+       if !ok || streamSchema == nil || streamSchema.stream == nil {
+               return "", false
+       }
+       tagFamilies := streamSchema.stream.GetTagFamilies()
+       if tagSpec.TagFamilyIdx < 0 || tagSpec.TagFamilyIdx >= len(tagFamilies) 
{
+               return "", false
+       }
+       return tagFamilies[tagSpec.TagFamilyIdx].GetName(), true
+}
diff --git a/pkg/query/logical/stream/stream_plan_tag_filter_test.go 
b/pkg/query/logical/stream/stream_plan_tag_filter_test.go
new file mode 100644
index 00000000..d2e68f63
--- /dev/null
+++ b/pkg/query/logical/stream/stream_plan_tag_filter_test.go
@@ -0,0 +1,152 @@
+// 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.
+
+package stream
+
+import (
+       "testing"
+       "time"
+
+       commonv1 
"github.com/apache/skywalking-banyandb/api/proto/banyandb/common/v1"
+       databasev1 
"github.com/apache/skywalking-banyandb/api/proto/banyandb/database/v1"
+       modelv1 
"github.com/apache/skywalking-banyandb/api/proto/banyandb/model/v1"
+       streamv1 
"github.com/apache/skywalking-banyandb/api/proto/banyandb/stream/v1"
+       "github.com/apache/skywalking-banyandb/pkg/query/logical"
+)
+
+func TestAnalyzeAddsHiddenCriteriaTags(t *testing.T) {
+       schema := mustBuildTestStreamSchema(t)
+       metadata := &commonv1.Metadata{Name: "svc", Group: "default"}
+       plan := tagFilter(
+               time.Unix(0, 0),
+               time.Unix(1, 0),
+               metadata,
+               buildEqualityCriteria("filter_tag", "match"),
+               [][]*logical.Tag{logical.NewTags("default", "projected_tag")},
+               nil,
+       )
+       resolved, err := plan.Analyze(schema)
+       if err != nil {
+               t.Fatalf("analyze tag filter: %v", err)
+       }
+
+       tfPlan, ok := resolved.(*tagFilterPlan)
+       if !ok {
+               t.Fatalf("expected tagFilterPlan, got %T", resolved)
+       }
+       if !tfPlan.hiddenTags.Contains("filter_tag") {
+               t.Fatalf("expected filter_tag to be tracked as hidden")
+       }
+
+       parent, ok := tfPlan.parent.(*localIndexScan)
+       if !ok {
+               t.Fatalf("expected localIndexScan as parent, got %T", 
tfPlan.parent)
+       }
+       found := false
+       for _, family := range parent.projectionTags {
+               for _, tagName := range family.Names {
+                       if tagName == "filter_tag" {
+                               found = true
+                               break
+                       }
+               }
+       }
+       if !found {
+               t.Fatalf("expected filter_tag to be added to projection for 
evaluation")
+       }
+}
+
+func TestStripHiddenTagsRemovesSensitiveValues(t *testing.T) {
+       hiddenTags := logical.NewHiddenTagSet()
+       hiddenTags.Add("hidden")
+
+       element := &streamv1.Element{
+               TagFamilies: []*modelv1.TagFamily{
+                       {
+                               Name: "default",
+                               Tags: []*modelv1.Tag{
+                                       {Key: "visible", Value: 
buildStringValue("keep")},
+                                       {Key: "hidden", Value: 
buildStringValue("drop")},
+                               },
+                       },
+                       {
+                               Name: "second",
+                               Tags: []*modelv1.Tag{
+                                       {Key: "hidden", Value: 
buildStringValue("drop")},
+                               },
+                       },
+               },
+       }
+
+       // Use the shared utility to strip hidden tags
+       element.TagFamilies = hiddenTags.StripHiddenTags(element.TagFamilies)
+
+       if len(element.GetTagFamilies()) != 1 {
+               t.Fatalf("expected only one tag family after stripping, got 
%d", len(element.GetTagFamilies()))
+       }
+       remainingTags := element.GetTagFamilies()[0].GetTags()
+       if len(remainingTags) != 1 || remainingTags[0].GetKey() != "visible" {
+               t.Fatalf("unexpected tags remaining after stripping: %+v", 
remainingTags)
+       }
+}
+
+func mustBuildTestStreamSchema(t *testing.T) logical.Schema {
+       t.Helper()
+       stream := &databasev1.Stream{
+               Entity: &databasev1.Entity{
+                       TagNames: []string{"entity_id"},
+               },
+               TagFamilies: []*databasev1.TagFamilySpec{
+                       {
+                               Name: "default",
+                               Tags: []*databasev1.TagSpec{
+                                       {Name: "filter_tag", Type: 
databasev1.TagType_TAG_TYPE_STRING},
+                                       {Name: "projected_tag", Type: 
databasev1.TagType_TAG_TYPE_STRING},
+                               },
+                       },
+               },
+       }
+       schema, err := BuildSchema(stream, nil)
+       if err != nil {
+               t.Fatalf("build schema: %v", err)
+       }
+       return schema
+}
+
+func buildEqualityCriteria(tagName, value string) *modelv1.Criteria {
+       return &modelv1.Criteria{
+               Exp: &modelv1.Criteria_Condition{
+                       Condition: &modelv1.Condition{
+                               Name: tagName,
+                               Op:   modelv1.Condition_BINARY_OP_EQ,
+                               Value: &modelv1.TagValue{
+                                       Value: &modelv1.TagValue_Str{
+                                               Str: &modelv1.Str{Value: value},
+                                       },
+                               },
+                       },
+               },
+       }
+}
+
+func buildStringValue(val string) *modelv1.TagValue {
+       return &modelv1.TagValue{
+               Value: &modelv1.TagValue_Str{
+                       Str: &modelv1.Str{Value: val},
+               },
+       }
+}
diff --git a/test/cases/measure/data/input/filter_hidden_tag.ql 
b/test/cases/measure/data/input/filter_hidden_tag.ql
new file mode 100644
index 00000000..bc532640
--- /dev/null
+++ b/test/cases/measure/data/input/filter_hidden_tag.ql
@@ -0,0 +1,22 @@
+# 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.
+
+# Test: Filter by service_id (indexed hidden tag) but only select id, 
entity_id (visible tags)
+SELECT id, entity_id, total::field, value::field FROM MEASURE 
service_instance_cpm_minute IN sw_metric
+TIME > '-15m'
+WHERE service_id = 'svc_1'
+
diff --git a/test/cases/measure/data/input/filter_hidden_tag.yaml 
b/test/cases/measure/data/input/filter_hidden_tag.yaml
new file mode 100644
index 00000000..46d8fb69
--- /dev/null
+++ b/test/cases/measure/data/input/filter_hidden_tag.yaml
@@ -0,0 +1,35 @@
+# 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.
+
+# Test: Filter by service_id (indexed hidden tag) but only project id, 
entity_id (visible tags)
+# This tests the hidden tag stripping in normal measure mode
+name: "service_instance_cpm_minute"
+groups: ["sw_metric"]
+tagProjection:
+  tagFamilies:
+  - name: "default"
+    tags: ["id", "entity_id"]
+fieldProjection:
+  names: ["total", "value"]
+criteria:
+  condition:
+    name: "service_id"
+    op: "BINARY_OP_EQ"
+    value:
+      str:
+        value: "svc_1"
+
diff --git a/test/cases/measure/data/input/index_mode_filter_hidden_tag.ql 
b/test/cases/measure/data/input/index_mode_filter_hidden_tag.ql
new file mode 100644
index 00000000..e2dfc569
--- /dev/null
+++ b/test/cases/measure/data/input/index_mode_filter_hidden_tag.ql
@@ -0,0 +1,22 @@
+# 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.
+
+# Test: Filter by layer (indexed hidden tag) but only select id, service_id, 
name (visible tags)
+SELECT id, service_id, name FROM MEASURE service_traffic IN index_mode
+TIME > '-15m'
+WHERE layer = 1
+
diff --git a/test/cases/measure/data/input/index_mode_filter_hidden_tag.yaml 
b/test/cases/measure/data/input/index_mode_filter_hidden_tag.yaml
new file mode 100644
index 00000000..6f257245
--- /dev/null
+++ b/test/cases/measure/data/input/index_mode_filter_hidden_tag.yaml
@@ -0,0 +1,33 @@
+# 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.
+
+# Test: Filter by layer (indexed hidden tag) but only project id, service_id, 
name (visible tags)
+# This tests the hidden tag stripping in index mode measure
+name: "service_traffic"
+groups: [ "index_mode" ]
+tagProjection:
+  tagFamilies:
+  - name: "default"
+    tags: [ "id", "service_id", "name" ]
+criteria:
+  condition:
+    name: "layer"
+    op: "BINARY_OP_EQ"
+    value:
+      int:
+        value: 1
+
diff --git a/test/cases/measure/data/want/filter_hidden_tag.yaml 
b/test/cases/measure/data/want/filter_hidden_tag.yaml
new file mode 100644
index 00000000..7aa7c2d8
--- /dev/null
+++ b/test/cases/measure/data/want/filter_hidden_tag.yaml
@@ -0,0 +1,126 @@
+# 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.
+
+# Expected result: service_instance_cpm_minute data with service_id=svc_1
+# Note: service_id should NOT be in result (hidden tag stripped)
+# Results are sorted by sid, then by timestamp (due to DisOrder: true)
+dataPoints:
+# sid: 7941974329620232473
+- fields:
+  - name: total
+    value:
+      int:
+        value: "100"
+  - name: value
+    value:
+      int:
+        value: "11"
+  tagFamilies:
+  - name: default
+    tags:
+    - key: id
+      value:
+        str:
+          value: "10"
+    - key: entity_id
+      value:
+        str:
+          value: entity_3
+# sid: 8355698475044176280 (earlier timestamp)
+- fields:
+  - name: total
+    value:
+      int:
+        value: "100"
+  - name: value
+    value:
+      int:
+        value: "1"
+  tagFamilies:
+  - name: default
+    tags:
+    - key: id
+      value:
+        str:
+          value: "3"
+    - key: entity_id
+      value:
+        str:
+          value: entity_1
+# sid: 8355698475044176280 (later timestamp)
+- fields:
+  - name: total
+    value:
+      int:
+        value: "300"
+  - name: value
+    value:
+      int:
+        value: "6"
+  tagFamilies:
+  - name: default
+    tags:
+    - key: id
+      value:
+        str:
+          value: "3"
+    - key: entity_id
+      value:
+        str:
+          value: entity_1
+# sid: 11136573860836752918 (earlier timestamp)
+- fields:
+  - name: total
+    value:
+      int:
+        value: "100"
+  - name: value
+    value:
+      int:
+        value: "9"
+  tagFamilies:
+  - name: default
+    tags:
+    - key: id
+      value:
+        str:
+          value: "5"
+    - key: entity_id
+      value:
+        str:
+          value: entity_2
+# sid: 11136573860836752918 (later timestamp)
+- fields:
+  - name: total
+    value:
+      int:
+        value: "100"
+  - name: value
+    value:
+      int:
+        value: "3"
+  tagFamilies:
+  - name: default
+    tags:
+    - key: id
+      value:
+        str:
+          value: "5"
+    - key: entity_id
+      value:
+        str:
+          value: entity_2
diff --git a/test/cases/measure/data/want/index_mode_filter_hidden_tag.yaml 
b/test/cases/measure/data/want/index_mode_filter_hidden_tag.yaml
new file mode 100644
index 00000000..8fdfcaab
--- /dev/null
+++ b/test/cases/measure/data/want/index_mode_filter_hidden_tag.yaml
@@ -0,0 +1,38 @@
+# 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.
+
+# Expected result: Only service with layer=1 (service_1), layer should NOT be 
in result
+dataPoints:
+- sid: "15142466043926325685"
+  tagFamilies:
+  - name: default
+    tags:
+    - key: id
+      value:
+        str:
+          value: "1"
+    - key: service_id
+      value:
+        str:
+          value: service_1
+    - key: name
+      value:
+        str:
+          value: service_name_1
+  timestamp: "2024-11-15T01:02:00Z"
+  version: "1"
+
diff --git a/test/cases/measure/measure.go b/test/cases/measure/measure.go
index 274dd3cb..5e692974 100644
--- a/test/cases/measure/measure.go
+++ b/test/cases/measure/measure.go
@@ -40,6 +40,8 @@ var (
 )
 
 var _ = g.DescribeTable("Scanning Measures", verify,
+       g.Entry("filter hidden tag projection", helpers.Args{Input: 
"filter_hidden_tag", Duration: 25 * time.Minute, Offset: -20 * time.Minute, 
DisOrder: true}),
+       g.Entry("index mode filter hidden tag projection", helpers.Args{Input: 
"index_mode_filter_hidden_tag", Duration: 25 * time.Minute, Offset: -20 * 
time.Minute}),
        g.Entry("all", helpers.Args{Input: "all", Duration: 25 * time.Minute, 
Offset: -20 * time.Minute}),
        g.Entry("all only fields", helpers.Args{Input: "all_only_fields", 
Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
        g.Entry("the max limit", helpers.Args{Input: "all_max_limit", Want: 
"all", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
diff --git a/test/cases/stream/data/input/filter_hidden_tag.ql 
b/test/cases/stream/data/input/filter_hidden_tag.ql
new file mode 100644
index 00000000..960b0ca6
--- /dev/null
+++ b/test/cases/stream/data/input/filter_hidden_tag.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 trace_id, span_id FROM STREAM sw IN default
+TIME > '-15m'
+WHERE span_id = '1' AND start_time = 1622933202000000000
+
diff --git a/test/cases/stream/data/input/filter_hidden_tag.yaml 
b/test/cases/stream/data/input/filter_hidden_tag.yaml
new file mode 100644
index 00000000..73ded0f3
--- /dev/null
+++ b/test/cases/stream/data/input/filter_hidden_tag.yaml
@@ -0,0 +1,41 @@
+# 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: "sw"
+groups: ["default"]
+projection:
+  tagFamilies:
+  - name: "searchable"
+    tags: ["trace_id", "span_id"]
+criteria:
+  le:
+    op: "LOGICAL_OP_AND"
+    left:
+      condition:
+        name: "span_id"
+        op: "BINARY_OP_EQ"
+        value:
+          str:
+            value: "1"
+    right:
+      condition:
+        name: "start_time"
+        op: "BINARY_OP_EQ"
+        value:
+          int:
+            value: 1622933202000000000
+
diff --git a/test/cases/stream/data/want/filter_hidden_tag.yaml 
b/test/cases/stream/data/want/filter_hidden_tag.yaml
new file mode 100644
index 00000000..b1ac936c
--- /dev/null
+++ b/test/cases/stream/data/want/filter_hidden_tag.yaml
@@ -0,0 +1,43 @@
+# 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.
+
+elements:
+- elementId: "0978df79cf4ed409"
+  tagFamilies:
+  - name: searchable
+    tags:
+    - key: trace_id
+      value:
+        str:
+          value: "1"
+    - key: span_id
+      value:
+        str:
+          value: "1"
+- elementId: "4dd999e3502d728d"
+  tagFamilies:
+  - name: searchable
+    tags:
+    - key: trace_id
+      value:
+        str:
+          value: "2"
+    - key: span_id
+      value:
+        str:
+          value: "1"
+
diff --git a/test/cases/stream/stream.go b/test/cases/stream/stream.go
index 97ae14a5..c0715df0 100644
--- a/test/cases/stream/stream.go
+++ b/test/cases/stream/stream.go
@@ -67,6 +67,7 @@ var _ = g.DescribeTable("Scanning Streams", func(args 
helpers.Args) {
        g.Entry("global index", helpers.Args{Input: "global_index", Duration: 1 
* time.Hour}),
        g.Entry("multi-global index", helpers.Args{Input: "global_indices", 
Duration: 1 * time.Hour}),
        g.Entry("filter by non-indexed tag", helpers.Args{Input: "filter_tag", 
Duration: 1 * time.Hour}),
+       g.Entry("filter hidden tag projection", helpers.Args{Input: 
"filter_hidden_tag", Duration: 1 * time.Hour}),
        g.Entry("filter by non-indexed tag order by duration desc with limit 
3", helpers.Args{Input: "sort_duration_no_index_limit", Duration: 1 * 
time.Hour}),
        g.Entry("get empty result by non-indexed tag", helpers.Args{Input: 
"filter_tag_empty", Duration: 1 * time.Hour, WantEmpty: true}),
        g.Entry("get results by no non-index tag", helpers.Args{Input: 
"filter_no_indexed", Duration: 1 * time.Hour}),

Reply via email to