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 7ae98e0b6 feat: implement global tag type metadata for stream parts 
(#1184)
7ae98e0b6 is described below

commit 7ae98e0b6addfe216b32712c57c054eabee5a4c2
Author: Huang Youliang <[email protected]>
AuthorDate: Sat Jun 27 08:57:44 2026 +0800

    feat: implement global tag type metadata for stream parts (#1184)
    
    * feat: implement global tag type metadata for stream parts
    
    * Fixup
    
    ---------
    
    Co-authored-by: Gao Hongtao <[email protected]>
---
 banyand/stream/block.go               |  9 ++++
 banyand/stream/block_metadata.go      | 16 ++++++
 banyand/stream/block_test.go          | 15 ++++++
 banyand/stream/block_writer.go        | 11 +++-
 banyand/stream/file_part_sync_test.go | 30 ++++++++++-
 banyand/stream/merger.go              | 14 +++---
 banyand/stream/merger_test.go         |  8 +++
 banyand/stream/part.go                | 22 +++++++-
 banyand/stream/part_metadata.go       | 95 +++++++++++++++++++++++++++++++++++
 banyand/stream/part_test.go           |  6 +++
 banyand/stream/syncer.go              | 19 +++++++
 banyand/stream/write_data.go          | 20 +++++---
 12 files changed, 249 insertions(+), 16 deletions(-)

diff --git a/banyand/stream/block.go b/banyand/stream/block.go
index f167c603c..8c83f0d28 100644
--- a/banyand/stream/block.go
+++ b/banyand/stream/block.go
@@ -190,10 +190,19 @@ func (b *block) validate() {
 func (b *block) marshalTagFamily(tf tagFamily, bm *blockMetadata, ww *writers) 
{
        hw, w, fw := ww.getWriters(tf.name)
        cc := tf.tags
+       if bm.tagType == nil {
+               bm.tagType = make(tagType)
+       }
+       tagTypes := bm.tagType[tf.name]
+       if tagTypes == nil {
+               tagTypes = make(map[string]pbv1.ValueType, len(cc))
+               bm.tagType[tf.name] = tagTypes
+       }
        cfm := generateTagFamilyMetadata()
        cmm := cfm.resizeTagMetadata(len(cc))
        for i := range cc {
                cc[i].mustWriteTo(&cmm[i], w, fw)
+               tagTypes[cc[i].name] = cc[i].valueType
        }
        bb := bigValuePool.Generate()
        defer bigValuePool.Release(bb)
diff --git a/banyand/stream/block_metadata.go b/banyand/stream/block_metadata.go
index cd117c2f7..db44348f7 100644
--- a/banyand/stream/block_metadata.go
+++ b/banyand/stream/block_metadata.go
@@ -61,6 +61,7 @@ func (d *dataBlock) unmarshal(src []byte) []byte {
 
 type blockMetadata struct {
        tagFamilies           map[string]*dataBlock
+       tagType               tagType
        tagProjection         []model.TagProjection
        timestamps            timestampsMetadata
        elementIDs            elementIDsMetadata
@@ -75,6 +76,16 @@ func (bm *blockMetadata) copyFrom(src *blockMetadata) {
        bm.count = src.count
        bm.timestamps.copyFrom(&src.timestamps)
        bm.elementIDs.copyFrom(&src.elementIDs)
+       if src.tagType == nil {
+               bm.tagType = nil
+       } else {
+               if bm.tagType == nil {
+                       bm.tagType = make(tagType, len(src.tagType))
+               } else {
+                       bm.tagType.reset()
+               }
+               bm.tagType.copyFrom(src.tagType)
+       }
        for k, db := range src.tagFamilies {
                if bm.tagFamilies == nil {
                        bm.tagFamilies = make(map[string]*dataBlock)
@@ -102,6 +113,11 @@ func (bm *blockMetadata) reset() {
        bm.count = 0
        bm.timestamps.reset()
        bm.elementIDs.reset()
+       if bm.tagType == nil {
+               bm.tagType = make(tagType)
+       } else {
+               bm.tagType.reset()
+       }
        for k := range bm.tagFamilies {
                bm.tagFamilies[k].reset()
                delete(bm.tagFamilies, k)
diff --git a/banyand/stream/block_test.go b/banyand/stream/block_test.go
index a2c08b9a5..b5195d19e 100644
--- a/banyand/stream/block_test.go
+++ b/banyand/stream/block_test.go
@@ -22,6 +22,7 @@ import (
        "testing"
 
        "github.com/google/go-cmp/cmp"
+       "github.com/stretchr/testify/require"
 
        "github.com/apache/skywalking-banyandb/api/common"
        "github.com/apache/skywalking-banyandb/pkg/bytes"
@@ -340,6 +341,20 @@ func Test_marshalAndUnmarshalBlock(t *testing.T) {
        bm := blockMetadata{}
 
        b.mustWriteTo(sid, &bm, ww)
+       require.Equal(t, tagType{
+               "arrTag": {
+                       "strArrTag": pbv1.ValueTypeStrArr,
+                       "intArrTag": pbv1.ValueTypeInt64Arr,
+               },
+               "binaryTag": {
+                       "binaryTag": pbv1.ValueTypeBinaryData,
+               },
+               "singleTag": {
+                       "strTag":   pbv1.ValueTypeStr,
+                       "intTag":   pbv1.ValueTypeInt64,
+                       "floatTag": pbv1.ValueTypeFloat64,
+               },
+       }, bm.tagType)
 
        tagFamilyMetadataReaders := make(map[string]fs.Reader)
        tagFamilyReaders := make(map[string]fs.Reader)
diff --git a/banyand/stream/block_writer.go b/banyand/stream/block_writer.go
index fdc77df25..c870c773c 100644
--- a/banyand/stream/block_writer.go
+++ b/banyand/stream/block_writer.go
@@ -155,6 +155,7 @@ func (sw *writers) getWriters(tagName string) (*writer, 
*writer, *writer) {
 
 type blockWriter struct {
        writers                    writers
+       tagType                    tagType
        metaData                   []byte
        primaryBlockData           []byte
        primaryBlockMetadata       primaryBlockMetadata
@@ -173,6 +174,11 @@ type blockWriter struct {
 
 func (bw *blockWriter) reset() {
        bw.writers.reset()
+       if bw.tagType == nil {
+               bw.tagType = make(tagType)
+       } else {
+               bw.tagType.reset()
+       }
        bw.sidLast = 0
        bw.sidFirst = 0
        bw.minTimestampLast = 0
@@ -230,6 +236,7 @@ func (bw *blockWriter) mustWriteBlock(sid common.SeriesID, 
b *block) {
 
        bm := generateBlockMetadata()
        b.mustWriteTo(sid, bm, &bw.writers)
+       bw.tagType.copyFrom(bm.tagType)
        tm := &bm.timestamps
        if bw.totalCount == 0 || tm.min < bw.totalMinTimestamp {
                bw.totalMinTimestamp = tm.min
@@ -271,7 +278,7 @@ func (bw *blockWriter) mustFlushPrimaryBlock(data []byte) {
        bw.sidFirst = 0
 }
 
-func (bw *blockWriter) Flush(pm *partMetadata) {
+func (bw *blockWriter) Flush(pm *partMetadata, tt *tagType) {
        pm.UncompressedSizeBytes = bw.totalUncompressedSizeBytes
        pm.TotalCount = bw.totalCount
        pm.BlocksCount = bw.totalBlocksCount
@@ -286,6 +293,7 @@ func (bw *blockWriter) Flush(pm *partMetadata) {
        bigValuePool.Release(bb)
 
        pm.CompressedSizeBytes = bw.writers.totalBytesWritten()
+       tt.copyFrom(bw.tagType)
 
        bw.writers.MustClose()
        bw.reset()
@@ -295,6 +303,7 @@ func generateBlockWriter() *blockWriter {
        v := blockWriterPool.Get()
        if v == nil {
                return &blockWriter{
+                       tagType: make(tagType),
                        writers: writers{
                                tagFamilyMetadataWriters: 
make(map[string]*writer),
                                tagFamilyWriters:         
make(map[string]*writer),
diff --git a/banyand/stream/file_part_sync_test.go 
b/banyand/stream/file_part_sync_test.go
index bc1c47076..0404d3342 100644
--- a/banyand/stream/file_part_sync_test.go
+++ b/banyand/stream/file_part_sync_test.go
@@ -26,6 +26,7 @@ import (
 
        "github.com/apache/skywalking-banyandb/api/common"
        "github.com/apache/skywalking-banyandb/banyand/internal/storage"
+       "github.com/apache/skywalking-banyandb/banyand/queue"
        "github.com/apache/skywalking-banyandb/pkg/convert"
        "github.com/apache/skywalking-banyandb/pkg/fs"
        "github.com/apache/skywalking-banyandb/pkg/logger"
@@ -93,8 +94,27 @@ func TestMustAddFilePart_FilesOnDisk(t *testing.T) {
        partID := uint64(1)
        destPath := partPath(tabDir, partID)
        buildAndFlushStreamMemPart(t, fileSystem, es, destPath)
-
-       tst.mustAddFilePart(partID)
+       var metadata partMetadata
+       metadata.mustReadMetadata(fileSystem, destPath)
+       partCtx := &syncPartContext{
+               tsTable:    tst,
+               fileSystem: fileSystem,
+               partPath:   destPath,
+               partMeta:   metadata,
+               partID:     partID,
+       }
+       chunkCtx := &queue.ChunkedSyncPartContext{Handler: partCtx, FileName: 
tagTypeFilename}
+       wantTagType := tagType{"singleTag": {"strTag": 
pbv1.ValueTypeStr}}.marshal()
+       callback := &syncCallback{l: logger.GetLogger("test")}
+       require.NoError(t, callback.HandleFileChunk(chunkCtx, 
wantTagType[:len(wantTagType)/2]))
+       require.NoError(t, callback.HandleFileChunk(chunkCtx, 
wantTagType[len(wantTagType)/2:]))
+       require.Equal(t, wantTagType, partCtx.tagTypeBuffer)
+       require.NoError(t, partCtx.FinishSync())
+       require.Nil(t, partCtx.tagTypeBuffer)
+
+       gotTagType, readErr := fileSystem.Read(filepath.Join(destPath, 
tagTypeFilename))
+       require.NoError(t, readErr)
+       require.Equal(t, wantTagType, gotTagType)
 
        snp := tst.currentSnapshot()
        require.NotNil(t, snp, "snapshot must not be nil after mustAddFilePart")
@@ -178,11 +198,17 @@ func TestFilePart_ErrorCleanup(t *testing.T) {
        spc.writers = w
 
        spc.writers.metaWriter.MustWrite([]byte("partial"))
+       tagTypeData := tagType{"singleTag": {"strTag": 
pbv1.ValueTypeStr}}.marshal()
+       callback := &syncCallback{l: logger.GetLogger("test")}
+       chunkCtx := &queue.ChunkedSyncPartContext{Handler: spc, FileName: 
tagTypeFilename}
+       require.NoError(t, callback.HandleFileChunk(chunkCtx, tagTypeData))
+       require.Equal(t, tagTypeData, spc.tagTypeBuffer)
 
        _, statErr := os.Stat(destPath)
        require.NoError(t, statErr, "part directory must exist before Close")
 
        require.NoError(t, spc.Close())
+       require.Nil(t, spc.tagTypeBuffer)
 
        _, statErr = os.Stat(destPath)
        require.True(t, os.IsNotExist(statErr), "incomplete part directory must 
be removed after Close without FinishSync")
diff --git a/banyand/stream/merger.go b/banyand/stream/merger.go
index 934eb43cb..05dc9bd5f 100644
--- a/banyand/stream/merger.go
+++ b/banyand/stream/merger.go
@@ -249,7 +249,7 @@ func (tst *tsTable) mergeParts(fileSystem fs.FileSystem, 
closeCh <-chan struct{}
        bw := generateBlockWriter()
        bw.mustInitForFilePart(fileSystem, dstPath, shouldCache)
 
-       pm, err := mergeBlocks(closeCh, bw, br)
+       pm, mergedTagType, err := mergeBlocks(closeCh, bw, br)
        releaseBlockWriter(bw)
        releaseBlockReader(br)
        for i := range pii {
@@ -259,6 +259,7 @@ func (tst *tsTable) mergeParts(fileSystem fs.FileSystem, 
closeCh <-chan struct{}
                return nil, err
        }
        pm.mustWriteMetadata(fileSystem, dstPath)
+       mergedTagType.mustWriteTagType(fileSystem, dstPath)
        // No SyncPath: mustWriteMetadata goes through WriteAtomic which already
        // fsyncs the parent directory after rename.
        p := mustOpenFilePart(partID, root, fileSystem)
@@ -268,7 +269,7 @@ func (tst *tsTable) mergeParts(fileSystem fs.FileSystem, 
closeCh <-chan struct{}
 
 var errClosed = fmt.Errorf("the merger is closed")
 
-func mergeBlocks(closeCh <-chan struct{}, bw *blockWriter, br *blockReader) 
(*partMetadata, error) {
+func mergeBlocks(closeCh <-chan struct{}, bw *blockWriter, br *blockReader) 
(*partMetadata, *tagType, error) {
        pendingBlockIsEmpty := true
        pendingBlock := generateBlockPointer()
        defer releaseBlockPointer(pendingBlock)
@@ -289,7 +290,7 @@ func mergeBlocks(closeCh <-chan struct{}, bw *blockWriter, 
br *blockReader) (*pa
        for br.nextBlockMetadata() {
                select {
                case <-closeCh:
-                       return nil, errClosed
+                       return nil, nil, errClosed
                default:
                }
                b := br.block
@@ -333,15 +334,16 @@ func mergeBlocks(closeCh <-chan struct{}, bw 
*blockWriter, br *blockReader) (*pa
                pendingBlockIsEmpty = true
        }
        if err := br.error(); err != nil {
-               return nil, fmt.Errorf("cannot read block to merge: %w", err)
+               return nil, nil, fmt.Errorf("cannot read block to merge: %w", 
err)
        }
        if !pendingBlockIsEmpty {
                bw.mustWriteBlock(pendingBlock.bm.seriesID, &pendingBlock.block)
        }
        releaseDecoder()
        var result partMetadata
-       bw.Flush(&result)
-       return &result, nil
+       mergedTagType := make(tagType)
+       bw.Flush(&result, &mergedTagType)
+       return &result, &mergedTagType, nil
 }
 
 func mergeTwoBlocks(target, left, right *blockPointer) {
diff --git a/banyand/stream/merger_test.go b/banyand/stream/merger_test.go
index 11c6022e8..5062f8f03 100644
--- a/banyand/stream/merger_test.go
+++ b/banyand/stream/merger_test.go
@@ -19,6 +19,7 @@ package stream
 
 import (
        "errors"
+       "path/filepath"
        "reflect"
        "testing"
 
@@ -317,6 +318,13 @@ func Test_mergeParts(t *testing.T) {
                                ); diff != "" {
                                        t.Errorf("Unexpected blockMetadata 
(-got +want):\n%s", diff)
                                }
+
+                               tagTypeData, readErr := 
fileSystem.Read(filepath.Join(partPath(root, partID), tagTypeFilename))
+                               require.NoError(t, readErr)
+                               mergedTagType := make(tagType)
+                               require.NoError(t, 
mergedTagType.unmarshal(tagTypeData))
+                               require.Equal(t, pbv1.ValueTypeStrArr, 
mergedTagType["arrTag"]["strArrTag"])
+                               require.Equal(t, pbv1.ValueTypeInt64, 
mergedTagType["singleTag"]["intTag"])
                        }
 
                        t.Run("memory parts", func(t *testing.T) {
diff --git a/banyand/stream/part.go b/banyand/stream/part.go
index 7ee11504c..7fac7392f 100644
--- a/banyand/stream/part.go
+++ b/banyand/stream/part.go
@@ -45,6 +45,7 @@ const (
        streamSeriesMetadataName = "smeta"
 
        metadataFilename               = "metadata.json"
+       tagTypeFilename                = "tag.type"
        primaryFilename                = streamPrimaryName + ".bin"
        metaFilename                   = streamMetaName + ".bin"
        timestampsFilename             = streamTimestampsName + ".bin"
@@ -118,6 +119,7 @@ type memPart struct {
        tagFamilyMetadata map[string]*bytes.Buffer
        tagFamilies       map[string]*bytes.Buffer
        tagFamilyFilter   map[string]*bytes.Buffer
+       tagType           tagType
        meta              bytes.Buffer
        primary           bytes.Buffer
        timestamps        bytes.Buffer
@@ -149,6 +151,11 @@ func (mp *memPart) mustCreateMemTagFamilyWriters(name 
string) (fs.Writer, fs.Wri
 
 func (mp *memPart) reset() {
        mp.partMetadata.reset()
+       if mp.tagType == nil {
+               mp.tagType = make(tagType)
+       } else {
+               mp.tagType.reset()
+       }
        mp.meta.Reset()
        mp.primary.Reset()
        mp.timestamps.Reset()
@@ -203,7 +210,7 @@ func (mp *memPart) mustInitFromElements(es *elements) {
                uncompressedBlockSizeBytes += uncompressedElementSizeBytes(i, 
es)
        }
        bsw.MustWriteElements(sidPrev, es.timestamps[indexPrev:], 
es.elementIDs[indexPrev:], es.tagFamilies[indexPrev:])
-       bsw.Flush(&mp.partMetadata)
+       bsw.Flush(&mp.partMetadata, &mp.tagType)
        releaseBlockWriter(bsw)
 }
 
@@ -231,6 +238,7 @@ func (mp *memPart) mustFlush(fileSystem fs.FileSystem, path 
string) {
                fs.MustFlushAtomic(fileSystem, mp.seriesMetadata.Buf, 
filepath.Join(path, seriesMetadataFilename), storage.FilePerm)
        }
 
+       mp.tagType.mustWriteTagType(fileSystem, path)
        mp.partMetadata.mustWriteMetadata(fileSystem, path)
        // No SyncPath: mustWriteMetadata goes through WriteAtomic which already
        // fsyncs the parent directory after rename, covering the dirent changes
@@ -422,6 +430,18 @@ func CreatePartFileReaderFromPath(partPath string, lfs 
fs.FileSystem) ([]queue.F
                if e.IsDir() {
                        continue
                }
+               if e.Name() == tagTypeFilename {
+                       tagTypePath := path.Join(partPath, e.Name())
+                       tagTypeReader, openErr := lfs.OpenFile(tagTypePath)
+                       if openErr != nil {
+                               logger.Panicf("cannot open tag type file %q: 
%s", tagTypePath, openErr)
+                       }
+                       readers = append(readers, tagTypeReader)
+                       files = append(files, queue.FileInfo{
+                               Name:   tagTypeFilename,
+                               Reader: tagTypeReader.SequentialRead(),
+                       })
+               }
                if filepath.Ext(e.Name()) == tagFamiliesMetadataFilenameExt {
                        tfmPath := path.Join(partPath, e.Name())
                        tfmReader, err := lfs.OpenFile(tfmPath)
diff --git a/banyand/stream/part_metadata.go b/banyand/stream/part_metadata.go
index 47cddb501..c4edcfdd3 100644
--- a/banyand/stream/part_metadata.go
+++ b/banyand/stream/part_metadata.go
@@ -19,14 +19,19 @@ package stream
 
 import (
        "encoding/json"
+       "fmt"
        "path/filepath"
+       "sort"
 
        "github.com/pkg/errors"
 
        "github.com/apache/skywalking-banyandb/banyand/internal/storage"
        "github.com/apache/skywalking-banyandb/banyand/queue"
+       "github.com/apache/skywalking-banyandb/pkg/convert"
+       "github.com/apache/skywalking-banyandb/pkg/encoding"
        "github.com/apache/skywalking-banyandb/pkg/fs"
        "github.com/apache/skywalking-banyandb/pkg/logger"
+       pbv1 "github.com/apache/skywalking-banyandb/pkg/pb/v1"
 )
 
 type partMetadata struct {
@@ -107,6 +112,96 @@ func (pm *partMetadata) mustWriteMetadata(fileSystem 
fs.FileSystem, partPath str
        }
 }
 
+type tagType map[string]map[string]pbv1.ValueType
+
+func (tt tagType) reset() {
+       clear(tt)
+}
+
+func (tt tagType) copyFrom(source tagType) {
+       for familyName, sourceTags := range source {
+               tags := tt[familyName]
+               if tags == nil {
+                       tags = make(map[string]pbv1.ValueType, len(sourceTags))
+                       tt[familyName] = tags
+               }
+               for tagName, valueType := range sourceTags {
+                       tags[tagName] = valueType
+               }
+       }
+}
+
+func (tt tagType) marshal() []byte {
+       var dst []byte
+       dst = encoding.VarUint64ToBytes(dst, uint64(len(tt)))
+       familyNames := make([]string, 0, len(tt))
+       for familyName := range tt {
+               familyNames = append(familyNames, familyName)
+       }
+       sort.Strings(familyNames)
+       for _, familyName := range familyNames {
+               dst = encoding.EncodeBytes(dst, 
convert.StringToBytes(familyName))
+               tags := tt[familyName]
+               dst = encoding.VarUint64ToBytes(dst, uint64(len(tags)))
+               tagNames := make([]string, 0, len(tags))
+               for tagName := range tags {
+                       tagNames = append(tagNames, tagName)
+               }
+               sort.Strings(tagNames)
+               for _, tagName := range tagNames {
+                       dst = encoding.EncodeBytes(dst, 
convert.StringToBytes(tagName))
+                       dst = append(dst, byte(tags[tagName]))
+               }
+       }
+       return dst
+}
+
+func (tt tagType) unmarshal(src []byte) error {
+       tt.reset()
+       remaining, familyCount := encoding.BytesToVarUint64(src)
+       for i := uint64(0); i < familyCount; i++ {
+               var familyNameBytes []byte
+               var decodeErr error
+               remaining, familyNameBytes, decodeErr = 
encoding.DecodeBytes(remaining)
+               if decodeErr != nil {
+                       return fmt.Errorf("cannot decode tag family name: %w", 
decodeErr)
+               }
+               var tagCount uint64
+               remaining, tagCount = encoding.BytesToVarUint64(remaining)
+               tags := make(map[string]pbv1.ValueType, tagCount)
+               for j := uint64(0); j < tagCount; j++ {
+                       var tagNameBytes []byte
+                       remaining, tagNameBytes, decodeErr = 
encoding.DecodeBytes(remaining)
+                       if decodeErr != nil {
+                               return fmt.Errorf("cannot decode tag name: %w", 
decodeErr)
+                       }
+                       if len(remaining) < 1 {
+                               return errors.New("insufficient data for tag 
value type")
+                       }
+                       tags[string(tagNameBytes)] = 
pbv1.ValueType(remaining[0])
+                       remaining = remaining[1:]
+               }
+               tt[string(familyNameBytes)] = tags
+       }
+       return nil
+}
+
+func (tt tagType) mustWriteTagType(fileSystem fs.FileSystem, partPath string) {
+       if len(tt) == 0 {
+               return
+       }
+       data := tt.marshal()
+       tagTypePath := filepath.Join(partPath, tagTypeFilename)
+       written, writeErr := fileSystem.WriteAtomic(data, tagTypePath, 
storage.FilePerm)
+       if writeErr != nil {
+               logger.Panicf("cannot write tag type: %s", writeErr)
+               return
+       }
+       if written != len(data) {
+               logger.Panicf("unexpected number of bytes written to %s; got 
%d; want %d", tagTypePath, written, len(data))
+       }
+}
+
 // ParsePartMetadata parses the part metadata from the metadata.json file.
 func ParsePartMetadata(fileSystem fs.FileSystem, partPath string) 
(queue.StreamingPartData, error) {
        metadataPath := filepath.Join(partPath, metadataFilename)
diff --git a/banyand/stream/part_test.go b/banyand/stream/part_test.go
index 638bde1ea..da521b850 100644
--- a/banyand/stream/part_test.go
+++ b/banyand/stream/part_test.go
@@ -114,6 +114,12 @@ func TestMustInitFromElements(t *testing.T) {
                                        _, ok = p.tagFamilyMetadata[k]
                                        require.True(t, ok, 
"p.tagFamilyMetadata %s not found", k)
                                }
+                               tagTypeData, readErr := 
fileSystem.Read(filepath.Join(path, tagTypeFilename))
+                               require.NoError(t, readErr)
+                               gotTagType := make(tagType)
+                               require.NoError(t, 
gotTagType.unmarshal(tagTypeData))
+                               require.Equal(t, pbv1.ValueTypeStrArr, 
gotTagType["arrTag"]["strArrTag"])
+                               require.Equal(t, pbv1.ValueTypeInt64Arr, 
gotTagType["arrTag"]["intArrTag"])
                        }
                })
        }
diff --git a/banyand/stream/syncer.go b/banyand/stream/syncer.go
index 33c10c13e..fc5dcfb2d 100644
--- a/banyand/stream/syncer.go
+++ b/banyand/stream/syncer.go
@@ -20,12 +20,14 @@ package stream
 import (
        "context"
        "fmt"
+       "path/filepath"
        "strconv"
        "time"
 
        "github.com/apache/skywalking-banyandb/api/data"
        "github.com/apache/skywalking-banyandb/banyand/internal/storage"
        "github.com/apache/skywalking-banyandb/banyand/queue"
+       "github.com/apache/skywalking-banyandb/pkg/bytes"
        "github.com/apache/skywalking-banyandb/pkg/compress/zstd"
        "github.com/apache/skywalking-banyandb/pkg/watcher"
 )
@@ -102,6 +104,7 @@ func (tst *tsTable) syncLoop(syncCh chan *syncIntroduction, 
flusherNotifier watc
 
 func createPartFileReaders(part *part) ([]queue.FileInfo, func()) {
        var files []queue.FileInfo
+       var buffersToRelease []*bytes.Buffer
 
        buf := bigValuePool.Generate()
        // Stream metadata
@@ -150,8 +153,24 @@ func createPartFileReaders(part *part) ([]queue.FileInfo, 
func()) {
                }
        }
 
+       if part.fileSystem != nil {
+               tagTypePath := filepath.Join(part.path, tagTypeFilename)
+               if tagTypeData, readErr := part.fileSystem.Read(tagTypePath); 
readErr == nil && len(tagTypeData) > 0 {
+                       tagTypeBuffer := bigValuePool.Generate()
+                       tagTypeBuffer.Buf = append(tagTypeBuffer.Buf[:0], 
tagTypeData...)
+                       buffersToRelease = append(buffersToRelease, 
tagTypeBuffer)
+                       files = append(files, queue.FileInfo{
+                               Name:   tagTypeFilename,
+                               Reader: tagTypeBuffer.SequentialRead(),
+                       })
+               }
+       }
+
        return files, func() {
                bigValuePool.Release(bb)
+               for _, buffer := range buffersToRelease {
+                       bigValuePool.Release(buffer)
+               }
        }
 }
 
diff --git a/banyand/stream/write_data.go b/banyand/stream/write_data.go
index af360c9c4..7a92aac25 100644
--- a/banyand/stream/write_data.go
+++ b/banyand/stream/write_data.go
@@ -20,6 +20,7 @@ package stream
 import (
        "context"
        "fmt"
+       "path/filepath"
        "strings"
        "sync/atomic"
        "time"
@@ -40,12 +41,13 @@ type syncPartContext struct {
        // can drive refCount to 0; releasing earlier lets each sync call 
bottom out
        // there, so the next session's incRef re-runs initTSTable and deletes 
the
        // in-flight part.
-       segment    storage.Segment[*tsTable, *commonv1.ResourceOpts]
-       fileSystem fs.FileSystem
-       writers    *writers
-       partPath   string
-       partMeta   partMetadata
-       partID     uint64
+       segment       storage.Segment[*tsTable, *commonv1.ResourceOpts]
+       fileSystem    fs.FileSystem
+       writers       *writers
+       partPath      string
+       tagTypeBuffer []byte
+       partMeta      partMetadata
+       partID        uint64
 }
 
 func (s *syncPartContext) NewPartType(_ *queue.ChunkedSyncPartContext) error {
@@ -57,6 +59,9 @@ func (s *syncPartContext) FinishSync() error {
        // Close writers first so file data is flushed before we write metadata.
        s.releaseCoreWriters()
 
+       if len(s.tagTypeBuffer) > 0 {
+               fs.MustFlushAtomic(s.fileSystem, s.tagTypeBuffer, 
filepath.Join(s.partPath, tagTypeFilename), storage.FilePerm)
+       }
        s.partMeta.mustWriteMetadata(s.fileSystem, s.partPath)
        // No SyncPath: mustWriteMetadata goes through WriteAtomic which already
        // fsyncs the parent directory after rename.
@@ -89,6 +94,7 @@ func (s *syncPartContext) Close() error {
        }
        s.tsTable = nil
        s.fileSystem = nil
+       s.tagTypeBuffer = nil
        return nil
 }
 
@@ -166,6 +172,8 @@ func (s *syncCallback) HandleFileChunk(ctx 
*queue.ChunkedSyncPartContext, chunk
        // Select the appropriate writer based on the filename and write the 
chunk.
        fileName := ctx.FileName
        switch {
+       case fileName == tagTypeFilename:
+               partCtx.tagTypeBuffer = append(partCtx.tagTypeBuffer, chunk...)
        case fileName == streamMetaName:
                partCtx.writers.metaWriter.MustWrite(chunk)
        case fileName == streamPrimaryName:

Reply via email to