Files
wehub-resource-sync 498b235461
Build and test / Build and test AMD64 Ubuntu 22.04 (push) Failing after 0s
Publish Builder / amazonlinux2023 (push) Failing after 1s
Build and test / UT for Go (push) Has been skipped
Publish KRTE Images / KRTE (push) Failing after 1s
Build and test / Integration Test (push) Has been skipped
Build and test / Upload Code Coverage (push) Has been skipped
Publish Builder / rockylinux9 (push) Failing after 1s
Publish Builder / ubuntu22.04 (push) Failing after 0s
Publish Builder / ubuntu24.04 (push) Failing after 0s
Publish Gpu Builder / publish-gpu-builder (push) Failing after 1s
Publish Test Images / PyTest (push) Failing after 0s
Build and test / UT for Cpp (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:17 +08:00

2370 lines
74 KiB
Go

// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The 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 importv2
import (
"context"
"fmt"
"testing"
"github.com/bytedance/mockey"
"github.com/cockroachdb/errors"
"github.com/stretchr/testify/assert"
"github.com/milvus-io/milvus/internal/compaction"
"github.com/milvus-io/milvus/internal/mocks"
"github.com/milvus-io/milvus/internal/storage"
"github.com/milvus-io/milvus/internal/storagev2/packed"
"github.com/milvus-io/milvus/pkg/v3/proto/datapb"
"github.com/milvus-io/milvus/pkg/v3/proto/indexpb"
)
func TestGenerateTargetPath(t *testing.T) {
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
tests := []struct {
name string
sourcePath string
wantPath string
wantErr bool
}{
{
name: "insert binlog path",
sourcePath: "files/insert_log/111/222/333/100/log1.log",
wantPath: "files/insert_log/444/555/666/100/log1.log",
wantErr: false,
},
{
name: "delta binlog path",
sourcePath: "files/delta_log/111/222/333/100/log1.log",
wantPath: "files/delta_log/444/555/666/100/log1.log",
wantErr: false,
},
{
name: "stats binlog path",
sourcePath: "files/stats_log/111/222/333/100/log1.log",
wantPath: "files/stats_log/444/555/666/100/log1.log",
wantErr: false,
},
{
name: "bm25 binlog path",
sourcePath: "files/bm25_stats/111/222/333/100/log1.log",
wantPath: "files/bm25_stats/444/555/666/100/log1.log",
wantErr: false,
},
{
name: "v3 bm25 stats numeric log id path",
sourcePath: "files/insert_log/111/222/333/_stats/bm25.100/12345",
wantPath: "files/insert_log/444/555/666/_stats/bm25.100/12345",
wantErr: false,
},
{
name: "external component in regular binlog root is not external table",
sourcePath: "files/external/insert_log/111/222/333/100/log1.log",
wantPath: "files/external/insert_log/444/555/666/100/log1.log",
wantErr: false,
},
{
name: "invalid path - no log type",
sourcePath: "files/111/222/333/100/log1.log",
wantPath: "",
wantErr: true,
},
{
name: "invalid path - too short",
sourcePath: "files/insert_log/111",
wantPath: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotPath, err := generateTargetPath(tt.sourcePath, source, target)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.wantPath, gotPath)
}
})
}
}
func TestGenerateMappingsFromFiles_ExternalTable(t *testing.T) {
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
StorageVersion: storage.StorageV3,
ManifestPath: packed.MarshalManifestPath("files/insert_log/111/222/333", 1),
IsExternalCollection: true,
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
files := &SegmentFiles{
InsertBinlogs: []string{"files/insert_log/111/222/333/_metadata/manifest.json"},
}
mappings, err := generateMappingsFromFiles(files, source, target)
assert.NoError(t, err)
assert.Equal(t,
"files/insert_log/444/555/666/_metadata/manifest.json",
mappings["files/insert_log/111/222/333/_metadata/manifest.json"])
}
func TestGenerateTargetLOBPath(t *testing.T) {
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
tests := []struct {
name string
sourcePath string
wantPath string
wantErr bool
}{
{
name: "standard LOB file path",
sourcePath: "files/insert_log/111/222/lobs/100/_data/abc123.vx",
wantPath: "files/insert_log/444/555/lobs/100/_data/abc123.vx",
wantErr: false,
},
{
name: "LOB file with nested field path",
sourcePath: "root/data/insert_log/111/222/lobs/200/_data/xyz.vx",
wantPath: "root/data/insert_log/444/555/lobs/200/_data/xyz.vx",
wantErr: false,
},
{
name: "invalid path - no insert_log",
sourcePath: "files/other/111/222/lobs/100/_data/abc.vx",
wantPath: "",
wantErr: true,
},
{
name: "invalid path - too short after insert_log",
sourcePath: "files/insert_log/111",
wantPath: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotPath, err := generateTargetLOBPath(tt.sourcePath, source, target)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.wantPath, gotPath)
}
})
}
}
func TestLobFileInfosToPaths(t *testing.T) {
tests := []struct {
name string
infos []packed.LobFileInfo
expected []string
}{
{
name: "empty input",
infos: nil,
expected: []string{},
},
{
name: "single LOB file - absolute path preserved",
infos: []packed.LobFileInfo{
{Path: "root/insert_log/100/200/lobs/300/_data/abc.vx", FieldID: 300, TotalRows: 1000, ValidRows: 900},
},
expected: []string{"root/insert_log/100/200/lobs/300/_data/abc.vx"},
},
{
name: "multiple LOB files from same segment",
infos: []packed.LobFileInfo{
{Path: "root/insert_log/100/200/lobs/300/_data/file1.vx", FieldID: 300, TotalRows: 500, ValidRows: 500},
{Path: "root/insert_log/100/200/lobs/300/_data/file2.vx", FieldID: 300, TotalRows: 500, ValidRows: 400},
{Path: "root/insert_log/100/200/lobs/301/_data/file3.vx", FieldID: 301, TotalRows: 200, ValidRows: 200},
},
expected: []string{
"root/insert_log/100/200/lobs/300/_data/file1.vx",
"root/insert_log/100/200/lobs/300/_data/file2.vx",
"root/insert_log/100/200/lobs/301/_data/file3.vx",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := lobFileInfosToPaths(tt.infos)
if len(tt.expected) == 0 {
assert.Empty(t, result)
} else {
assert.Equal(t, tt.expected, result)
}
})
}
}
// TestCollectSegmentFiles_LOBFromManifest verifies that LOB file collection
// reads from the segment manifest (not the partition directory), ensuring
// only this segment's LOB files are collected.
func TestCollectSegmentFiles_LOBFromManifest(t *testing.T) {
// This segment owns only 2 LOB files
segmentLobFiles := []packed.LobFileInfo{
{Path: "root/insert_log/100/200/lobs/300/_data/seg1_file1.vx", FieldID: 300, TotalRows: 1000, ValidRows: 1000},
{Path: "root/insert_log/100/200/lobs/300/_data/seg1_file2.vx", FieldID: 300, TotalRows: 500, ValidRows: 500},
}
// Mock CreateStorageConfig to avoid paramtable dependency
mocker0 := mockey.Mock(compaction.CreateStorageConfig).Return(&indexpb.StorageConfig{}).Build()
defer mocker0.UnPatch()
// Mock GetManifestLobFiles to return only this segment's LOB files
mocker1 := mockey.Mock(packed.GetManifestLobFiles).Return(segmentLobFiles, nil).Build()
defer mocker1.UnPatch()
// Mock UnmarshalManifestPath
mocker2 := mockey.Mock(packed.UnmarshalManifestPath).Return("root/insert_log/100/200/1001", int64(1), nil).Build()
defer mocker2.UnPatch()
// Mock listAllFiles for insert binlogs (via WalkWithPrefix)
mocker3 := mockey.Mock(listAllFiles).Return(
[]string{"root/insert_log/100/200/1001/_data/cg0.parquet"}, nil).Build()
defer mocker3.UnPatch()
source := &datapb.CopySegmentSource{
SegmentId: 1001,
CollectionId: 100,
PartitionId: 200,
StorageVersion: storage.StorageV3,
ManifestPath: `{"basePath":"root/insert_log/100/200/1001","version":1}`,
}
files, err := collectSegmentFiles(context.Background(), nil, source)
assert.NoError(t, err)
assert.NotNil(t, files)
// Verify LOB files come from manifest, not from directory listing.
// GetManifestLobFiles returns absolute paths (after ToAbsolutePaths in C++),
// so lobFileInfosToPaths uses them directly.
assert.Equal(t, 2, len(files.LobFiles))
assert.Equal(t, "root/insert_log/100/200/lobs/300/_data/seg1_file1.vx", files.LobFiles[0])
assert.Equal(t, "root/insert_log/100/200/lobs/300/_data/seg1_file2.vx", files.LobFiles[1])
// Key invariant: only this segment's files are returned.
// The old code used listAllFiles(partition/lobs/) which would have also
// returned seg2_file3.vx, seg2_file4.vx from other segments.
}
func TestGenerateTargetIndexPath(t *testing.T) {
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
tests := []struct {
name string
sourcePath string
indexType string
pathVersion indexpb.IndexStorePathVersion
wantPath string
wantErr bool
}{
{
name: "vector scalar index path v0 format",
sourcePath: "files/index_files/1001/1/222/333/scalar_index",
indexType: IndexTypeVectorScalarV0,
pathVersion: indexpb.IndexStorePathVersion_INDEX_STORE_PATH_VERSION_BUILD_ROOTED,
wantPath: "files/index_files/1001/1/555/666/scalar_index",
wantErr: false,
},
{
name: "text index path",
sourcePath: "files/text_log/123/1/111/222/333/100/index_file",
indexType: IndexTypeText,
wantPath: "files/text_log/123/1/444/555/666/100/index_file",
wantErr: false,
},
{
name: "json key index path",
sourcePath: "files/json_key_index_log/123/1/111/222/333/100/index_file",
indexType: IndexTypeJSONKey,
wantPath: "files/json_key_index_log/123/1/444/555/666/100/index_file",
wantErr: false,
},
{
name: "json stats path - shared_key_index",
sourcePath: "files/json_stats/2/123/1/111/222/333/100/shared_key_index/index_file",
indexType: IndexTypeJSONStats,
wantPath: "files/json_stats/2/123/1/444/555/666/100/shared_key_index/index_file",
wantErr: false,
},
{
name: "json stats path - shredding_data",
sourcePath: "files/json_stats/2/123/1/111/222/333/100/shredding_data/data_file",
indexType: IndexTypeJSONStats,
wantPath: "files/json_stats/2/123/1/444/555/666/100/shredding_data/data_file",
wantErr: false,
},
{
name: "invalid - keyword not found",
sourcePath: "files/other_index/111/222/333/100/index",
indexType: IndexTypeVectorScalarV0,
wantPath: "",
wantErr: true,
},
{
name: "invalid - path too short",
sourcePath: "files/index_files/111",
indexType: IndexTypeVectorScalarV0,
wantPath: "",
wantErr: true,
},
{
name: "invalid - unsupported index type",
sourcePath: "files/unknown_index/111/222/333/100/index",
indexType: "unknown_type",
wantPath: "",
wantErr: true,
},
{
name: "vector scalar index path v1 format",
sourcePath: "files/index_v1/111/222/333/1001/1/scalar_index",
indexType: IndexTypeVectorScalarV0,
pathVersion: indexpb.IndexStorePathVersion_INDEX_STORE_PATH_VERSION_COLLECTION_ROOTED,
wantPath: "files/index_v1/444/555/666/1001/1/scalar_index",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotPath, err := generateTargetIndexPath(tt.sourcePath, source, target, tt.indexType, tt.pathVersion)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.wantPath, gotPath)
}
})
}
}
// TestGenerateTargetIndexPath_BuildIDMapping verifies that NewBuildIds mapping
// is applied at the correct offset for each index path format.
// Regression test: v1 VectorScalar places buildID at offset 4 (not 1).
func TestGenerateTargetIndexPath_BuildIDMapping(t *testing.T) {
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
NewBuildIds: map[int64]int64{1001: 2001},
}
tests := []struct {
name string
sourcePath string
indexType string
pathVersion indexpb.IndexStorePathVersion
wantPath string
}{
{
name: "v0 vector scalar maps buildID at offset 1",
sourcePath: "files/index_files/1001/1/222/333/scalar_index",
indexType: IndexTypeVectorScalarV0,
pathVersion: indexpb.IndexStorePathVersion_INDEX_STORE_PATH_VERSION_BUILD_ROOTED,
wantPath: "files/index_files/2001/1/555/666/scalar_index",
},
{
name: "v1 vector scalar maps buildID at offset 4",
sourcePath: "files/index_v1/111/222/333/1001/1/scalar_index",
indexType: IndexTypeVectorScalarV0,
pathVersion: indexpb.IndexStorePathVersion_INDEX_STORE_PATH_VERSION_COLLECTION_ROOTED,
wantPath: "files/index_v1/444/555/666/2001/1/scalar_index",
},
{
name: "text index maps buildID at offset 1",
sourcePath: "files/text_log/1001/1/111/222/333/100/idx",
indexType: IndexTypeText,
wantPath: "files/text_log/2001/1/444/555/666/100/idx",
},
{
name: "json stats maps buildID at offset 2",
sourcePath: "files/json_stats/2/1001/1/111/222/333/100/shared_key_index/idx",
indexType: IndexTypeJSONStats,
wantPath: "files/json_stats/2/2001/1/444/555/666/100/shared_key_index/idx",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := generateTargetIndexPath(tt.sourcePath, source, target, tt.indexType, tt.pathVersion)
assert.NoError(t, err)
assert.Equal(t, tt.wantPath, got)
})
}
}
func TestGenerateMappingsFromFiles_VectorScalarUsesSourcePathVersion(t *testing.T) {
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
IndexFiles: []*indexpb.IndexFilePathInfo{
{
BuildID: 1001,
IndexVersion: 1,
IndexStorePathVersion: indexpb.IndexStorePathVersion_INDEX_STORE_PATH_VERSION_BUILD_ROOTED,
IndexFilePaths: []string{"files/index_files/1001/1/222/333/v0_file"},
},
{
BuildID: 1002,
IndexVersion: 1,
IndexStorePathVersion: indexpb.IndexStorePathVersion_INDEX_STORE_PATH_VERSION_COLLECTION_ROOTED,
IndexFilePaths: []string{"files/index_v1/111/222/333/1002/1/v1_file"},
},
},
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
NewBuildIds: map[int64]int64{1001: 2001, 1002: 2002},
}
files := &SegmentFiles{
VectorScalarIndex: []string{
"files/index_files/1001/1/222/333/v0_file",
"files/index_v1/111/222/333/1002/1/v1_file",
},
}
mappings, err := generateMappingsFromFiles(files, source, target)
assert.NoError(t, err)
assert.Equal(t, "files/index_files/2001/1/555/666/v0_file", mappings["files/index_files/1001/1/222/333/v0_file"])
assert.Equal(t, "files/index_v1/444/555/666/2002/1/v1_file", mappings["files/index_v1/111/222/333/1002/1/v1_file"])
}
func TestTransformFieldBinlogs(t *testing.T) {
mappings := map[string]string{
"files/insert_log/111/222/333/100/log1.log": "files/insert_log/444/555/666/100/log1.log",
"files/insert_log/111/222/333/101/log2.log": "files/insert_log/444/555/666/101/log2.log",
}
srcFieldBinlogs := []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{
EntriesNum: 1000,
TimestampFrom: 100,
TimestampTo: 200,
LogPath: "files/insert_log/111/222/333/100/log1.log",
LogSize: 1024,
MemorySize: 1024,
},
},
},
{
FieldID: 101,
Binlogs: []*datapb.Binlog{
{
EntriesNum: 2000,
TimestampFrom: 150,
TimestampTo: 250,
LogPath: "files/insert_log/111/222/333/101/log2.log",
LogSize: 2048,
MemorySize: 2048,
},
},
},
}
t.Run("count rows for insert logs", func(t *testing.T) {
result, totalRows, err := transformFieldBinlogs(srcFieldBinlogs, mappings, true, false)
assert.NoError(t, err)
assert.Equal(t, int64(3000), totalRows)
assert.Equal(t, 2, len(result))
// Verify first field binlog
assert.Equal(t, int64(100), result[0].FieldID)
assert.Equal(t, 1, len(result[0].Binlogs))
assert.Equal(t, int64(1000), result[0].Binlogs[0].EntriesNum)
assert.Equal(t, "files/insert_log/444/555/666/100/log1.log", result[0].Binlogs[0].LogPath)
assert.Equal(t, int64(1024), result[0].Binlogs[0].LogSize)
assert.Equal(t, int64(1024), result[0].Binlogs[0].MemorySize)
// Verify second field binlog
assert.Equal(t, int64(101), result[1].FieldID)
assert.Equal(t, 1, len(result[1].Binlogs))
assert.Equal(t, int64(2000), result[1].Binlogs[0].EntriesNum)
})
t.Run("no row counting for stats logs", func(t *testing.T) {
result, totalRows, err := transformFieldBinlogs(srcFieldBinlogs, mappings, false, false)
assert.NoError(t, err)
assert.Equal(t, int64(0), totalRows)
assert.Equal(t, 2, len(result))
})
t.Run("skip binlogs with empty path", func(t *testing.T) {
srcWithEmpty := []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{
EntriesNum: 1000,
LogPath: "",
},
},
},
}
result, _, err := transformFieldBinlogs(srcWithEmpty, mappings, false, false)
assert.NoError(t, err)
assert.Equal(t, 0, len(result))
})
t.Run("error on missing mapping", func(t *testing.T) {
srcWithUnmapped := []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{
EntriesNum: 1000,
LogPath: "files/insert_log/999/888/777/100/unmapped.log",
},
},
},
}
result, totalRows, err := transformFieldBinlogs(srcWithUnmapped, mappings, true, false)
assert.Error(t, err)
assert.Contains(t, err.Error(), "no mapping found")
assert.Nil(t, result)
assert.Equal(t, int64(0), totalRows)
})
}
func TestCopySegmentAndIndexFiles(t *testing.T) {
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
InsertBinlogs: []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{
EntriesNum: 1000,
LogPath: "files/insert_log/111/222/333/100/12345",
LogSize: 1024,
},
},
},
},
IndexFiles: []*indexpb.IndexFilePathInfo{
{
FieldID: 100,
IndexID: 1001,
BuildID: 1002,
IndexFilePaths: []string{"files/index_files/1002/1/222/333/index1"},
},
},
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
t.Run("successful copy", func(t *testing.T) {
mCopy := mockey.Mock(copyFile).Return(nil).Build()
defer mCopy.UnPatch()
cm := &struct{ storage.ChunkManager }{}
result, copiedFiles, err := CopySegmentAndIndexFiles(context.Background(), cm, source, target, nil)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, int64(666), result.SegmentId)
assert.Equal(t, int64(1000), result.ImportedRows)
assert.Equal(t, 1, len(result.Binlogs))
assert.Equal(t, 1, len(result.IndexInfos))
assert.Len(t, copiedFiles, 2)
})
t.Run("copy failure", func(t *testing.T) {
mCopy := mockey.Mock(copyFile).Return(errors.New("copy failed")).Build()
defer mCopy.UnPatch()
cm := &struct{ storage.ChunkManager }{}
result, copiedFiles, err := CopySegmentAndIndexFiles(context.Background(), cm, source, target, nil)
assert.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "failed to copy file")
assert.Empty(t, copiedFiles)
})
}
func TestGenerateSegmentInfoFromSource(t *testing.T) {
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
InsertBinlogs: []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{
EntriesNum: 1000,
TimestampFrom: 100,
TimestampTo: 200,
LogPath: "files/insert_log/111/222/333/100/log1.log",
LogSize: 1024,
},
},
},
},
StatsBinlogs: []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{
LogPath: "files/stats_log/111/222/333/100/stats1.log",
},
},
},
},
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
mappings := map[string]string{
"files/insert_log/111/222/333/100/log1.log": "files/insert_log/444/555/666/100/log1.log",
"files/stats_log/111/222/333/100/stats1.log": "files/stats_log/444/555/666/100/stats1.log",
}
t.Run("generate segment info", func(t *testing.T) {
segmentInfo, err := generateSegmentInfoFromSource(source, target, mappings)
assert.NoError(t, err)
assert.NotNil(t, segmentInfo)
assert.Equal(t, int64(666), segmentInfo.SegmentID)
assert.Equal(t, int64(1000), segmentInfo.ImportedRows)
assert.Equal(t, 1, len(segmentInfo.Binlogs))
assert.Equal(t, 1, len(segmentInfo.Statslogs))
assert.Equal(t, "files/insert_log/444/555/666/100/log1.log", segmentInfo.Binlogs[0].Binlogs[0].LogPath)
})
}
func TestBuildIndexInfoFromSource(t *testing.T) {
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
IndexFiles: []*indexpb.IndexFilePathInfo{
{
FieldID: 100,
IndexID: 1001,
BuildID: 1002,
IndexFilePaths: []string{"files/index_files/1002/1/222/333/index1"},
SerializedSize: 5000,
},
},
TextIndexFiles: map[int64]*datapb.TextIndexStats{
100: {
FieldID: 100,
Version: 1,
BuildID: 2001,
Files: []string{"files/text_log/123/1/111/222/333/100/text1"},
LogSize: 2048,
MemorySize: 4096,
},
},
JsonKeyIndexFiles: map[int64]*datapb.JsonKeyStats{
101: {
FieldID: 101,
Version: 1,
BuildID: 3001,
JsonKeyStatsDataFormat: 1, // Legacy format
Files: []string{"files/json_key_index_log/123/1/111/222/333/101/json1"},
MemorySize: 3072,
},
102: {
FieldID: 102,
Version: 1,
BuildID: 3002,
JsonKeyStatsDataFormat: 2, // New format
Files: []string{
"files/json_stats/2/3002/1/111/222/333/102/shared_key_index/index1",
"files/json_stats/2/3002/1/111/222/333/102/shredding_data/data1",
},
MemorySize: 4096,
},
},
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
mappings := map[string]string{
"files/index_files/1002/1/222/333/index1": "files/index_files/1002/1/555/666/index1",
"files/text_log/123/1/111/222/333/100/text1": "files/text_log/123/1/444/555/666/100/text1",
"files/json_key_index_log/123/1/111/222/333/101/json1": "files/json_key_index_log/123/1/444/555/666/101/json1",
"files/json_stats/2/3002/1/111/222/333/102/shared_key_index/index1": "files/json_stats/2/3002/1/444/555/666/102/shared_key_index/index1",
"files/json_stats/2/3002/1/111/222/333/102/shredding_data/data1": "files/json_stats/2/3002/1/444/555/666/102/shredding_data/data1",
}
t.Run("error on missing vector index mapping", func(t *testing.T) {
emptyMappings := map[string]string{}
_, _, _, err := buildIndexInfoFromSource(source, target, emptyMappings)
assert.Error(t, err)
assert.Contains(t, err.Error(), "no mapping found for index file")
})
t.Run("error on missing text index mapping", func(t *testing.T) {
partialMappings := map[string]string{
"files/index_files/1002/1/222/333/index1": "files/index_files/1002/1/555/666/index1",
}
_, _, _, err := buildIndexInfoFromSource(source, target, partialMappings)
assert.Error(t, err)
assert.Contains(t, err.Error(), "no mapping found for text index file")
})
t.Run("error on missing json index mapping", func(t *testing.T) {
partialMappings := map[string]string{
"files/index_files/1002/1/222/333/index1": "files/index_files/1002/1/555/666/index1",
"files/text_log/123/1/111/222/333/100/text1": "files/text_log/123/1/444/555/666/100/text1",
}
_, _, _, err := buildIndexInfoFromSource(source, target, partialMappings)
assert.Error(t, err)
assert.Contains(t, err.Error(), "no mapping found for JSON index file")
})
t.Run("build all index info", func(t *testing.T) {
indexInfos, textIndexInfos, jsonKeyIndexInfos, err := buildIndexInfoFromSource(source, target, mappings)
assert.NoError(t, err)
// Verify vector/scalar index info (keyed by buildID, not fieldID)
assert.Equal(t, 1, len(indexInfos))
assert.NotNil(t, indexInfos[1002])
assert.Equal(t, int64(100), indexInfos[1002].FieldId)
assert.Equal(t, int64(1001), indexInfos[1002].IndexId)
assert.Equal(t, int64(1002), indexInfos[1002].BuildId)
assert.Equal(t, int64(5000), indexInfos[1002].IndexSize)
assert.Equal(t, "files/index_files/1002/1/555/666/index1", indexInfos[1002].IndexFilePaths[0])
// Verify text index info
assert.Equal(t, 1, len(textIndexInfos))
assert.NotNil(t, textIndexInfos[100])
assert.Equal(t, int64(100), textIndexInfos[100].FieldID)
assert.Equal(t, int64(2001), textIndexInfos[100].BuildID)
assert.Equal(t, "files/text_log/123/1/444/555/666/100/text1", textIndexInfos[100].Files[0])
// Verify JSON key index info (legacy and new formats)
assert.Equal(t, 2, len(jsonKeyIndexInfos))
// Legacy format (data_format = 1)
assert.NotNil(t, jsonKeyIndexInfos[101])
assert.Equal(t, int64(101), jsonKeyIndexInfos[101].FieldID)
assert.Equal(t, int64(3001), jsonKeyIndexInfos[101].BuildID)
assert.Equal(t, int64(1), jsonKeyIndexInfos[101].JsonKeyStatsDataFormat)
assert.Equal(t, "files/json_key_index_log/123/1/444/555/666/101/json1", jsonKeyIndexInfos[101].Files[0])
// New format (data_format = 2)
assert.NotNil(t, jsonKeyIndexInfos[102])
assert.Equal(t, int64(102), jsonKeyIndexInfos[102].FieldID)
assert.Equal(t, int64(3002), jsonKeyIndexInfos[102].BuildID)
assert.Equal(t, int64(2), jsonKeyIndexInfos[102].JsonKeyStatsDataFormat)
assert.Equal(t, 2, len(jsonKeyIndexInfos[102].Files))
assert.Equal(t, "files/json_stats/2/3002/1/444/555/666/102/shared_key_index/index1", jsonKeyIndexInfos[102].Files[0])
assert.Equal(t, "files/json_stats/2/3002/1/444/555/666/102/shredding_data/data1", jsonKeyIndexInfos[102].Files[1])
})
}
func TestBuildIndexInfoFromSource_PreservesIndexStorePathVersion(t *testing.T) {
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
IndexFiles: []*indexpb.IndexFilePathInfo{
{
FieldID: 100,
IndexID: 10,
BuildID: 1001,
IndexName: "idx_v0",
IndexVersion: 1,
SerializedSize: 1024,
IndexStorePathVersion: indexpb.IndexStorePathVersion_INDEX_STORE_PATH_VERSION_BUILD_ROOTED,
IndexFilePaths: []string{"files/index_files/1001/1/222/333/v0_file"},
},
{
FieldID: 101,
IndexID: 11,
BuildID: 1002,
IndexName: "idx_v1",
IndexVersion: 1,
SerializedSize: 2048,
IndexStorePathVersion: indexpb.IndexStorePathVersion_INDEX_STORE_PATH_VERSION_COLLECTION_ROOTED,
IndexFilePaths: []string{"files/index_v1/111/222/333/1002/1/v1_file"},
},
},
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
NewBuildIds: map[int64]int64{1001: 2001, 1002: 2002},
}
mappings := map[string]string{
"files/index_files/1001/1/222/333/v0_file": "files/index_files/2001/1/555/666/v0_file",
"files/index_v1/111/222/333/1002/1/v1_file": "files/index_v1/444/555/666/2002/1/v1_file",
}
indexInfos, _, _, err := buildIndexInfoFromSource(source, target, mappings)
assert.NoError(t, err)
assert.Equal(t, indexpb.IndexStorePathVersion_INDEX_STORE_PATH_VERSION_BUILD_ROOTED, indexInfos[2001].GetIndexStorePathVersion())
assert.Equal(t, indexpb.IndexStorePathVersion_INDEX_STORE_PATH_VERSION_COLLECTION_ROOTED, indexInfos[2002].GetIndexStorePathVersion())
}
func TestCopySegmentAndIndexFiles_ReturnsFileList(t *testing.T) {
t.Run("success returns all copied files", func(t *testing.T) {
mCopy := mockey.Mock(copyFile).Return(nil).Build()
defer mCopy.UnPatch()
cm := &struct{ storage.ChunkManager }{}
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
InsertBinlogs: []*datapb.FieldBinlog{
{
FieldID: 1,
Binlogs: []*datapb.Binlog{
{LogPath: "files/insert_log/111/222/333/1/10001", LogSize: 100},
{LogPath: "files/insert_log/111/222/333/1/10002", LogSize: 200},
},
},
},
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
result, copiedFiles, err := CopySegmentAndIndexFiles(context.Background(), cm, source, target, nil)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Len(t, copiedFiles, 2)
assert.Contains(t, copiedFiles, "files/insert_log/444/555/666/1/10001")
assert.Contains(t, copiedFiles, "files/insert_log/444/555/666/1/10002")
})
t.Run("failure returns partial file list", func(t *testing.T) {
callCount := 0
mCopy := mockey.Mock(copyFile).
To(func(_ context.Context, _ storage.ChunkManager, src, dst string) error {
callCount++
if callCount > 1 {
return errors.New("copy failed")
}
return nil
}).Build()
defer mCopy.UnPatch()
cm := &struct{ storage.ChunkManager }{}
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
InsertBinlogs: []*datapb.FieldBinlog{
{
FieldID: 1,
Binlogs: []*datapb.Binlog{
{LogPath: "files/insert_log/111/222/333/1/10001", LogSize: 100},
{LogPath: "files/insert_log/111/222/333/1/10002", LogSize: 200},
},
},
},
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
result, copiedFiles, err := CopySegmentAndIndexFiles(context.Background(), cm, source, target, nil)
assert.Error(t, err)
assert.Nil(t, result)
assert.True(t, len(copiedFiles) <= 1, "should return files copied before failure")
})
}
func TestGenerateTargetIndexPath_PathTooShort(t *testing.T) {
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
tests := []struct {
name string
path string
indexType string
}{
{
name: "text index path too short",
path: "files/text_log/123/1/111",
indexType: IndexTypeText,
},
{
name: "json key index path too short",
path: "files/json_key_index_log/123",
indexType: IndexTypeJSONKey,
},
{
name: "json stats path too short",
path: "files/json_stats/2/123/1/111",
indexType: IndexTypeJSONStats,
},
{
name: "vector scalar path exactly at keyword",
path: "files/index_files",
indexType: IndexTypeVectorScalarV0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := generateTargetIndexPath(tt.path, source, target, tt.indexType, indexpb.IndexStorePathVersion_INDEX_STORE_PATH_VERSION_BUILD_ROOTED)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid")
})
}
}
func TestGenerateTargetIndexPath_VectorScalarPreservesIDs(t *testing.T) {
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
// Verify buildID (1001) and indexVersion (1) are preserved, NOT replaced
// partitionID (222->555) and segmentID (333->666) ARE replaced
// collectionID is NOT in path at all
result, err := generateTargetIndexPath("files/index_files/1001/1/222/333/HNSW_SQ_3", source, target, IndexTypeVectorScalarV0, indexpb.IndexStorePathVersion_INDEX_STORE_PATH_VERSION_BUILD_ROOTED)
assert.NoError(t, err)
assert.Equal(t, "files/index_files/1001/1/555/666/HNSW_SQ_3", result)
// Verify collectionID (111) does NOT appear in target path
assert.NotContains(t, result, "111")
assert.NotContains(t, result, "444") // collectionID should NOT be injected
}
func TestGenerateTargetPath_AllConstants(t *testing.T) {
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
// Verify all constants match the correct storage path names
tests := []struct {
name string
logType string
expected string
}{
{"insert uses insert_log", BinlogTypeInsert, "insert_log"},
{"delta uses delta_log", BinlogTypeDelta, "delta_log"},
{"stats uses stats_log", BinlogTypeStats, "stats_log"},
{"bm25 uses bm25_stats", BinlogTypeBM25, "bm25_stats"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
path := fmt.Sprintf("files/%s/111/222/333/100/log1", tt.expected)
result, err := generateTargetPath(path, source, target)
assert.NoError(t, err)
assert.Contains(t, result, tt.expected)
assert.Contains(t, result, "444")
assert.Contains(t, result, "555")
assert.Contains(t, result, "666")
})
}
}
func TestGenerateSegmentInfoFromSource_AllBinlogTypes(t *testing.T) {
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
InsertBinlogs: []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{EntriesNum: 500, LogPath: "files/insert_log/111/222/333/100/log1"},
},
},
},
StatsBinlogs: []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{LogPath: "files/stats_log/111/222/333/100/stats1"},
},
},
},
DeltaBinlogs: []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{EntriesNum: 10, LogPath: "files/delta_log/111/222/333/100/delta1"},
},
},
},
Bm25Binlogs: []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{LogPath: "files/bm25_stats/111/222/333/100/bm25_1"},
},
},
},
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
mappings := map[string]string{
"files/insert_log/111/222/333/100/log1": "files/insert_log/444/555/666/100/log1",
"files/stats_log/111/222/333/100/stats1": "files/stats_log/444/555/666/100/stats1",
"files/delta_log/111/222/333/100/delta1": "files/delta_log/444/555/666/100/delta1",
"files/bm25_stats/111/222/333/100/bm25_1": "files/bm25_stats/444/555/666/100/bm25_1",
}
segmentInfo, err := generateSegmentInfoFromSource(source, target, mappings)
assert.NoError(t, err)
assert.NotNil(t, segmentInfo)
assert.Equal(t, int64(666), segmentInfo.SegmentID)
assert.Equal(t, int64(500), segmentInfo.ImportedRows) // Only insert rows counted
assert.Equal(t, 1, len(segmentInfo.Binlogs))
assert.Equal(t, 1, len(segmentInfo.Statslogs))
assert.Equal(t, 1, len(segmentInfo.Deltalogs))
assert.Equal(t, 1, len(segmentInfo.Bm25Logs))
assert.Equal(t, "files/delta_log/444/555/666/100/delta1", segmentInfo.Deltalogs[0].Binlogs[0].LogPath)
assert.Equal(t, "files/bm25_stats/444/555/666/100/bm25_1", segmentInfo.Bm25Logs[0].Binlogs[0].LogPath)
}
func TestGenerateSegmentInfoFromSource_EmptySource(t *testing.T) {
source := &datapb.CopySegmentSource{}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
segmentInfo, err := generateSegmentInfoFromSource(source, target, map[string]string{})
assert.NoError(t, err)
assert.NotNil(t, segmentInfo)
assert.Equal(t, int64(666), segmentInfo.SegmentID)
assert.Equal(t, int64(0), segmentInfo.ImportedRows)
assert.Equal(t, 0, len(segmentInfo.Binlogs))
assert.Equal(t, 0, len(segmentInfo.Statslogs))
assert.Equal(t, 0, len(segmentInfo.Deltalogs))
assert.Equal(t, 0, len(segmentInfo.Bm25Logs))
}
// TestBuildIndexInfoFromSource_MultipleIndexesPerField verifies that multiple indexes
// on the same field (e.g., JSON path indexes) are all preserved in the result map.
// This is the core scenario for the fix: using buildID as map key instead of fieldID.
func TestBuildIndexInfoFromSource_MultipleIndexesPerField(t *testing.T) {
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
IndexFiles: []*indexpb.IndexFilePathInfo{
{
FieldID: 100, // Same field
IndexID: 1001,
BuildID: 2001,
IndexName: "idx_category",
IndexFilePaths: []string{"files/index_files/2001/1/222/333/index1"},
SerializedSize: 3000,
},
{
FieldID: 100, // Same field, different path index
IndexID: 1002,
BuildID: 2002,
IndexName: "idx_price",
IndexFilePaths: []string{"files/index_files/2002/1/222/333/index2"},
SerializedSize: 4000,
},
},
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
NewBuildIds: map[int64]int64{
2001: 3001, // old buildID -> new buildID
2002: 3002,
},
}
mappings := map[string]string{
"files/index_files/2001/1/222/333/index1": "files/index_files/3001/1/555/666/index1",
"files/index_files/2002/1/222/333/index2": "files/index_files/3002/1/555/666/index2",
}
indexInfos, _, _, err := buildIndexInfoFromSource(source, target, mappings)
assert.NoError(t, err)
// Both indexes should be present (keyed by new buildID)
assert.Equal(t, 2, len(indexInfos), "Both JSON path indexes should be in the result")
// Verify first index (idx_category)
assert.NotNil(t, indexInfos[3001])
assert.Equal(t, int64(100), indexInfos[3001].FieldId)
assert.Equal(t, int64(1001), indexInfos[3001].IndexId)
assert.Equal(t, int64(3001), indexInfos[3001].BuildId)
assert.Equal(t, "idx_category", indexInfos[3001].IndexName)
// Verify second index (idx_price)
assert.NotNil(t, indexInfos[3002])
assert.Equal(t, int64(100), indexInfos[3002].FieldId)
assert.Equal(t, int64(1002), indexInfos[3002].IndexId)
assert.Equal(t, int64(3002), indexInfos[3002].BuildId)
assert.Equal(t, "idx_price", indexInfos[3002].IndexName)
}
func TestBuildIndexInfoFromSource_EmptySource(t *testing.T) {
source := &datapb.CopySegmentSource{}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
indexInfos, textIndexInfos, jsonKeyIndexInfos, err := buildIndexInfoFromSource(source, target, map[string]string{})
assert.NoError(t, err)
assert.Equal(t, 0, len(indexInfos))
assert.Equal(t, 0, len(textIndexInfos))
assert.Equal(t, 0, len(jsonKeyIndexInfos))
}
func TestBuildIndexInfoFromSource_UnmappedPaths(t *testing.T) {
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
t.Run("unmapped vector index path returns error", func(t *testing.T) {
source := &datapb.CopySegmentSource{
IndexFiles: []*indexpb.IndexFilePathInfo{
{
FieldID: 100,
IndexID: 1001,
BuildID: 1002,
IndexFilePaths: []string{"unmapped/path"},
SerializedSize: 5000,
},
},
}
_, _, _, err := buildIndexInfoFromSource(source, target, map[string]string{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "no mapping found for index file")
})
t.Run("unmapped text index path returns error", func(t *testing.T) {
source := &datapb.CopySegmentSource{
TextIndexFiles: map[int64]*datapb.TextIndexStats{
200: {
FieldID: 200,
Files: []string{"unmapped/text/path"},
},
},
}
_, _, _, err := buildIndexInfoFromSource(source, target, map[string]string{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "no mapping found for text index file")
})
t.Run("unmapped json index path returns error", func(t *testing.T) {
source := &datapb.CopySegmentSource{
JsonKeyIndexFiles: map[int64]*datapb.JsonKeyStats{
300: {
FieldID: 300,
Files: []string{"unmapped/json/path"},
},
},
}
_, _, _, err := buildIndexInfoFromSource(source, target, map[string]string{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "no mapping found for JSON index file")
})
}
func TestTransformFieldBinlogs_NilInput(t *testing.T) {
result, totalRows, err := transformFieldBinlogs(nil, map[string]string{}, true, false)
assert.NoError(t, err)
assert.Equal(t, int64(0), totalRows)
assert.Equal(t, 0, len(result))
}
func TestTransformFieldBinlogs_MultipleBinlogsPerField(t *testing.T) {
mappings := map[string]string{
"files/insert_log/111/222/333/100/log1": "files/insert_log/444/555/666/100/log1",
"files/insert_log/111/222/333/100/log2": "files/insert_log/444/555/666/100/log2",
"files/insert_log/111/222/333/100/log3": "files/insert_log/444/555/666/100/log3",
}
srcFieldBinlogs := []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{EntriesNum: 100, LogPath: "files/insert_log/111/222/333/100/log1", LogSize: 512},
{EntriesNum: 200, LogPath: "files/insert_log/111/222/333/100/log2", LogSize: 1024},
{EntriesNum: 300, LogPath: "files/insert_log/111/222/333/100/log3", LogSize: 2048},
},
},
}
result, totalRows, err := transformFieldBinlogs(srcFieldBinlogs, mappings, true, false)
assert.NoError(t, err)
assert.Equal(t, int64(600), totalRows)
assert.Equal(t, 1, len(result))
assert.Equal(t, 3, len(result[0].Binlogs))
assert.Equal(t, "files/insert_log/444/555/666/100/log1", result[0].Binlogs[0].LogPath)
assert.Equal(t, "files/insert_log/444/555/666/100/log2", result[0].Binlogs[1].LogPath)
assert.Equal(t, "files/insert_log/444/555/666/100/log3", result[0].Binlogs[2].LogPath)
}
func TestTransformFieldBinlogs_UnmappedPath(t *testing.T) {
// Path not in mappings -> returns error (fail-fast)
mappings := map[string]string{}
srcFieldBinlogs := []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{EntriesNum: 100, LogPath: "files/insert_log/111/222/333/100/log1"},
},
},
}
_, _, err := transformFieldBinlogs(srcFieldBinlogs, mappings, true, false)
assert.Error(t, err)
assert.Contains(t, err.Error(), "no mapping found for source path")
}
func TestCopySegmentAndIndexFiles_CreateFileMappingsError(t *testing.T) {
mockCM := mocks.NewChunkManager(t)
// Source with invalid path that will cause generateMappingsFromFiles to fail
source := &datapb.CopySegmentSource{
InsertBinlogs: []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{LogPath: "invalid/path/without/log/type"},
},
},
},
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
result, copiedFiles, err := CopySegmentAndIndexFiles(context.Background(), mockCM, source, target, nil)
assert.Error(t, err)
assert.Nil(t, result)
assert.Nil(t, copiedFiles)
assert.Contains(t, err.Error(), "failed to generate file mappings")
}
func TestShortenIndexFilePaths(t *testing.T) {
tests := []struct {
name string
fullPaths []string
expected []string
}{
{
name: "vector/scalar index paths",
fullPaths: []string{
"files/index_files/1001/1/555/666/scalar_index",
"files/index_files/1001/1/555/666/vector_index",
},
expected: []string{"scalar_index", "vector_index"},
},
{
name: "empty path list",
fullPaths: []string{},
expected: []string{},
},
{
name: "single file name",
fullPaths: []string{
"index_file",
},
expected: []string{"index_file"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := shortenIndexFilePaths(tt.fullPaths)
assert.Equal(t, tt.expected, result)
})
}
}
func TestShortenSingleJsonStatsPath(t *testing.T) {
tests := []struct {
name string
inputPath string
expectedPath string
}{
{
name: "shared_key_index path",
inputPath: "files/json_stats/2/123/1/444/555/666/100/shared_key_index/inverted_index_0",
expectedPath: "shared_key_index/inverted_index_0",
},
{
name: "shredding_data path",
inputPath: "files/json_stats/2/123/1/444/555/666/100/shredding_data/parquet_data_0",
expectedPath: "shredding_data/parquet_data_0",
},
{
name: "already shortened - shared_key_index",
inputPath: "shared_key_index/inverted_index_0",
expectedPath: "shared_key_index/inverted_index_0",
},
{
name: "already shortened - shredding_data",
inputPath: "shredding_data/parquet_data_0",
expectedPath: "shredding_data/parquet_data_0",
},
{
name: "no keyword - keep as-is",
inputPath: "files/other/path/file.json",
expectedPath: "files/other/path/file.json",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := shortenSingleJSONStatsPath(tt.inputPath)
assert.Equal(t, tt.expectedPath, result)
})
}
}
func TestShortenJsonStatsPath(t *testing.T) {
jsonStats := map[int64]*datapb.JsonKeyStats{
100: {
FieldID: 100,
Version: 1,
BuildID: 123,
Files: []string{
"files/json_stats/2/123/1/444/555/666/100/shared_key_index/inverted_index_0",
"files/json_stats/2/123/1/444/555/666/100/shared_key_index/inverted_index_1",
},
JsonKeyStatsDataFormat: 2,
MemorySize: 1024,
LogSize: 2048,
},
200: {
FieldID: 200,
Version: 1,
BuildID: 456,
Files: []string{
"files/json_stats/2/456/1/444/555/666/200/shredding_data/parquet_data_0",
},
JsonKeyStatsDataFormat: 2,
MemorySize: 512,
},
}
result := shortenJSONStatsPath(jsonStats)
assert.Equal(t, 2, len(result))
// Check field 100
assert.NotNil(t, result[100])
assert.Equal(t, int64(100), result[100].FieldID)
assert.Equal(t, int64(1), result[100].Version)
assert.Equal(t, int64(123), result[100].BuildID)
assert.Equal(t, int64(2), result[100].JsonKeyStatsDataFormat)
assert.Equal(t, int64(1024), result[100].MemorySize)
assert.Equal(t, int64(2048), result[100].LogSize)
assert.Equal(t, 2, len(result[100].Files))
assert.Equal(t, "shared_key_index/inverted_index_0", result[100].Files[0])
assert.Equal(t, "shared_key_index/inverted_index_1", result[100].Files[1])
// Check field 200
assert.NotNil(t, result[200])
assert.Equal(t, int64(200), result[200].FieldID)
assert.Equal(t, int64(1), result[200].Version)
assert.Equal(t, int64(456), result[200].BuildID)
assert.Equal(t, int64(2), result[200].JsonKeyStatsDataFormat)
assert.Equal(t, int64(512), result[200].MemorySize)
assert.Equal(t, 1, len(result[200].Files))
assert.Equal(t, "shredding_data/parquet_data_0", result[200].Files[0])
}
func TestShortenJsonStatsPath_MetaJson(t *testing.T) {
jsonStats := map[int64]*datapb.JsonKeyStats{
102: {
FieldID: 102,
Version: 1,
BuildID: 462930163709949539,
Files: []string{
"files/json_stats/2/462930163709949539/1/462930163710600038/462930163710600039/462930163710600046/102/meta.json",
},
JsonKeyStatsDataFormat: 2,
MemorySize: 256,
},
}
result := shortenJSONStatsPath(jsonStats)
assert.Equal(t, 1, len(result))
assert.NotNil(t, result[102])
assert.Equal(t, int64(102), result[102].FieldID)
assert.Equal(t, 1, len(result[102].Files))
assert.Equal(t, "meta.json", result[102].Files[0])
}
func TestShortenSingleJsonStatsPath_EdgeCases(t *testing.T) {
t.Run("already_shortened_meta", func(t *testing.T) {
result := shortenSingleJSONStatsPath("meta.json")
assert.Equal(t, "meta.json", result)
})
t.Run("already_shortened_shared_key", func(t *testing.T) {
result := shortenSingleJSONStatsPath("shared_key_index/inverted_index_0")
assert.Equal(t, "shared_key_index/inverted_index_0", result)
})
t.Run("full_path_meta_json", func(t *testing.T) {
fullPath := "files/json_stats/2/123/1/444/555/666/100/meta.json"
result := shortenSingleJSONStatsPath(fullPath)
assert.Equal(t, "meta.json", result)
})
t.Run("full_path_nested_file", func(t *testing.T) {
fullPath := "files/json_stats/2/123/1/444/555/666/100/subdir/file.dat"
result := shortenSingleJSONStatsPath(fullPath)
assert.Equal(t, "subdir/file.dat", result)
})
}
func TestTransformManifestPath(t *testing.T) {
target := &datapb.CopySegmentTarget{
SegmentId: 2001,
CollectionId: 111,
PartitionId: 222,
}
sourceManifest := packed.MarshalManifestPath(
"files/insert_log/449104612037410004/449104621518610066/449104621518610065",
2,
)
source := &datapb.CopySegmentSource{}
targetManifest, err := transformManifestPath(sourceManifest, source, target)
assert.NoError(t, err)
basePath, version, err := packed.UnmarshalManifestPath(targetManifest)
assert.NoError(t, err)
assert.Equal(t, int64(2), version)
assert.Contains(t, basePath, "111")
assert.Contains(t, basePath, "222")
assert.Contains(t, basePath, "2001")
}
func TestTransformManifestPath_ExternalTable(t *testing.T) {
target := &datapb.CopySegmentTarget{
SegmentId: 666,
CollectionId: 444,
PartitionId: 555,
}
sourceManifest := packed.MarshalManifestPath("files/insert_log/111/222/333", 2)
source := &datapb.CopySegmentSource{IsExternalCollection: true}
targetManifest, err := transformManifestPath(sourceManifest, source, target)
assert.NoError(t, err)
basePath, version, err := packed.UnmarshalManifestPath(targetManifest)
assert.NoError(t, err)
assert.Equal(t, int64(2), version)
assert.Equal(t, "files/insert_log/444/555/666", basePath)
}
func TestTransformManifestPath_LegacyExternalTableLayoutUnsupported(t *testing.T) {
target := &datapb.CopySegmentTarget{
SegmentId: 666,
CollectionId: 444,
PartitionId: 555,
}
sourceManifest := packed.MarshalManifestPath("external/111/segments/333", 2)
source := &datapb.CopySegmentSource{IsExternalCollection: true}
targetManifest, err := transformManifestPath(sourceManifest, source, target)
assert.Error(t, err)
assert.Empty(t, targetManifest)
assert.Contains(t, err.Error(), "invalid binlog path structure")
}
func TestTransformFieldBinlogs_SkipsPathMappingForExternalTable(t *testing.T) {
src := []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{
LogID: 10,
EntriesNum: 123,
MemorySize: 456,
LogSize: 456,
},
},
},
}
got, totalRows, err := transformFieldBinlogs(src, nil, true, true)
assert.NoError(t, err)
assert.Equal(t, int64(123), totalRows)
assert.Len(t, got, 1)
assert.Equal(t, int64(100), got[0].GetFieldID())
assert.Len(t, got[0].GetBinlogs(), 1)
assert.Empty(t, got[0].GetBinlogs()[0].GetLogPath())
assert.Equal(t, int64(10), got[0].GetBinlogs()[0].GetLogID())
assert.Equal(t, int64(123), got[0].GetBinlogs()[0].GetEntriesNum())
assert.Equal(t, int64(456), got[0].GetBinlogs()[0].GetMemorySize())
}
func TestTransformFieldBinlogs_SkipsEmptyPathForInternalTable(t *testing.T) {
src := []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{LogID: 10, EntriesNum: 123},
},
},
}
got, totalRows, err := transformFieldBinlogs(src, nil, false, false)
assert.NoError(t, err)
assert.Zero(t, totalRows)
assert.Empty(t, got)
}
func TestGenerateSegmentInfoFromSource_PreservesExternalTableBinlogMetadata(t *testing.T) {
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
StorageVersion: storage.StorageV3,
ManifestPath: packed.MarshalManifestPath("files/insert_log/111/222/333", 2),
IsExternalCollection: true,
InsertBinlogs: []*datapb.FieldBinlog{
{
FieldID: 0,
ChildFields: []int64{100, 101, 102},
Binlogs: []*datapb.Binlog{
{LogID: 10, EntriesNum: 123, MemorySize: 456},
},
},
},
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
segmentInfo, err := generateSegmentInfoFromSource(source, target, nil)
assert.NoError(t, err)
assert.Equal(t, int64(123), segmentInfo.GetImportedRows())
assert.Len(t, segmentInfo.GetBinlogs(), 1)
assert.Empty(t, segmentInfo.GetBinlogs()[0].GetBinlogs()[0].GetLogPath())
}
func TestCopySegmentAndIndexFiles_ExternalTable(t *testing.T) {
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
StorageVersion: storage.StorageV3,
ManifestPath: packed.MarshalManifestPath("files/insert_log/111/222/333", 2),
IsExternalCollection: true,
InsertBinlogs: []*datapb.FieldBinlog{
{
FieldID: 0,
ChildFields: []int64{100, 101, 102},
Binlogs: []*datapb.Binlog{
{LogID: 10, EntriesNum: 123, MemorySize: 456},
},
},
},
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
mList := mockey.Mock(listAllFiles).Return([]string{
"files/insert_log/111/222/333/_data/0",
"files/insert_log/111/222/333/_metadata/manifest.json",
}, nil).Build()
defer mList.UnPatch()
copied := make(map[string]string)
mCopy := mockey.Mock(copyFile).
To(func(_ context.Context, _ storage.ChunkManager, src, dst string) error {
copied[src] = dst
return nil
}).Build()
defer mCopy.UnPatch()
cm := &struct{ storage.ChunkManager }{}
result, copiedFiles, err := CopySegmentAndIndexFiles(context.Background(), cm, source, target, nil)
assert.NoError(t, err)
assert.Equal(t, int64(123), result.GetImportedRows())
assert.Equal(t, packed.MarshalManifestPath("files/insert_log/444/555/666", 2), result.GetManifestPath())
assert.Len(t, result.GetBinlogs(), 1)
assert.Empty(t, result.GetBinlogs()[0].GetBinlogs()[0].GetLogPath())
assert.Equal(t, "files/insert_log/444/555/666/_data/0", copied["files/insert_log/111/222/333/_data/0"])
assert.Equal(t, "files/insert_log/444/555/666/_metadata/manifest.json", copied["files/insert_log/111/222/333/_metadata/manifest.json"])
assert.ElementsMatch(t, []string{
"files/insert_log/444/555/666/_data/0",
"files/insert_log/444/555/666/_metadata/manifest.json",
}, copiedFiles)
}
func TestCollectSegmentFiles_WithManifest(t *testing.T) {
ctx := context.Background()
manifestPath := packed.MarshalManifestPath("files/insert_log/111/222/333", 2)
mList := mockey.Mock(listAllFiles).Return([]string{
"files/insert_log/111/222/333/_data/100/file1.log",
"files/insert_log/111/222/333/_data/101/file2.log",
"files/insert_log/111/222/333/_metadata/manifest.json",
}, nil).Build()
defer mList.UnPatch()
cm := &struct{ storage.ChunkManager }{}
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
StorageVersion: storage.StorageV3,
ManifestPath: manifestPath,
InsertBinlogs: []*datapb.FieldBinlog{},
}
files, err := collectSegmentFiles(ctx, cm, source)
assert.NoError(t, err)
assert.Len(t, files.InsertBinlogs, 3)
assert.Contains(t, files.InsertBinlogs, "files/insert_log/111/222/333/_data/100/file1.log")
assert.Contains(t, files.InsertBinlogs, "files/insert_log/111/222/333/_data/101/file2.log")
assert.Contains(t, files.InsertBinlogs, "files/insert_log/111/222/333/_metadata/manifest.json")
}
func TestCollectSegmentFiles_V3MissingManifestPath(t *testing.T) {
ctx := context.Background()
cm := &struct{ storage.ChunkManager }{}
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
StorageVersion: storage.StorageV3,
ManifestPath: "", // V3 but no manifest
}
files, err := collectSegmentFiles(ctx, cm, source)
assert.Error(t, err)
assert.Nil(t, files)
assert.Contains(t, err.Error(), "requires manifest_path but it is empty")
}
func TestCollectSegmentFiles_ManifestUnmarshalFailure(t *testing.T) {
ctx := context.Background()
cm := &struct{ storage.ChunkManager }{}
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
StorageVersion: storage.StorageV3,
ManifestPath: "invalid-json-not-a-manifest",
}
files, err := collectSegmentFiles(ctx, cm, source)
assert.Error(t, err)
assert.Nil(t, files)
assert.Contains(t, err.Error(), "failed to unmarshal manifest path")
}
func TestCollectSegmentFiles_ManifestListFailure(t *testing.T) {
ctx := context.Background()
manifestPath := packed.MarshalManifestPath("files/insert_log/111/222/333", 2)
mList := mockey.Mock(listAllFiles).Return(nil, errors.New("storage unavailable")).Build()
defer mList.UnPatch()
cm := &struct{ storage.ChunkManager }{}
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
StorageVersion: storage.StorageV3,
ManifestPath: manifestPath,
}
files, err := collectSegmentFiles(ctx, cm, source)
assert.Error(t, err)
assert.Nil(t, files)
assert.Contains(t, err.Error(), "failed to list files from manifest base path")
}
func TestCollectSegmentFiles_ManifestEmptyFiles(t *testing.T) {
ctx := context.Background()
manifestPath := packed.MarshalManifestPath("files/insert_log/111/222/333", 2)
mList := mockey.Mock(listAllFiles).Return([]string{}, nil).Build()
defer mList.UnPatch()
cm := &struct{ storage.ChunkManager }{}
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
StorageVersion: storage.StorageV3,
ManifestPath: manifestPath,
InsertBinlogs: []*datapb.FieldBinlog{}, // empty pb — should NOT be used
}
// Empty file list is OK for V3 — segment may have only deltas
files, err := collectSegmentFiles(ctx, cm, source)
assert.NoError(t, err)
assert.NotNil(t, files)
assert.Empty(t, files.InsertBinlogs)
}
func TestCollectSegmentFiles_NoManifest(t *testing.T) {
ctx := context.Background()
cm := &struct{ storage.ChunkManager }{}
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
StorageVersion: storage.StorageV1, // V1: use pb paths
InsertBinlogs: []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{LogPath: "files/insert_log/111/222/333/100/file1.log"},
{LogPath: "files/insert_log/111/222/333/100/file2.log"},
},
},
},
}
files, err := collectSegmentFiles(ctx, cm, source)
assert.NoError(t, err)
assert.Len(t, files.InsertBinlogs, 2)
assert.Contains(t, files.InsertBinlogs, "files/insert_log/111/222/333/100/file1.log")
assert.Contains(t, files.InsertBinlogs, "files/insert_log/111/222/333/100/file2.log")
}
func TestExtractFromPb(t *testing.T) {
t.Run("extract paths from field binlogs", func(t *testing.T) {
fieldBinlogs := []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{LogPath: "path/a"},
{LogPath: "path/b"},
},
},
{
FieldID: 101,
Binlogs: []*datapb.Binlog{
{LogPath: "path/c"},
},
},
}
paths := extractFromPb(fieldBinlogs)
assert.Equal(t, []string{"path/a", "path/b", "path/c"}, paths)
})
t.Run("skip empty paths", func(t *testing.T) {
fieldBinlogs := []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{LogPath: "path/a"},
{LogPath: ""},
},
},
}
paths := extractFromPb(fieldBinlogs)
assert.Equal(t, []string{"path/a"}, paths)
})
t.Run("empty input", func(t *testing.T) {
paths := extractFromPb(nil)
assert.Empty(t, paths)
})
}
func TestExtractIndexFiles(t *testing.T) {
t.Run("extract index file paths", func(t *testing.T) {
indexInfos := []*indexpb.IndexFilePathInfo{
{IndexFilePaths: []string{"idx/a", "idx/b"}},
{IndexFilePaths: []string{"idx/c"}},
}
paths := extractIndexFiles(indexInfos)
assert.Equal(t, []string{"idx/a", "idx/b", "idx/c"}, paths)
})
t.Run("empty input", func(t *testing.T) {
paths := extractIndexFiles(nil)
assert.Empty(t, paths)
})
}
func TestExtractTextIndexFiles(t *testing.T) {
t.Run("extract text index file paths", func(t *testing.T) {
textInfos := map[int64]*datapb.TextIndexStats{
100: {Files: []string{"text/a", "text/b"}},
}
paths := extractTextIndexFiles(textInfos)
assert.Equal(t, []string{"text/a", "text/b"}, paths)
})
t.Run("empty input", func(t *testing.T) {
paths := extractTextIndexFiles(nil)
assert.Empty(t, paths)
})
}
func TestExtractJsonFiles(t *testing.T) {
t.Run("separate by data format", func(t *testing.T) {
jsonInfos := map[int64]*datapb.JsonKeyStats{
100: {
JsonKeyStatsDataFormat: 1,
Files: []string{"legacy/a"},
},
101: {
JsonKeyStatsDataFormat: 2,
Files: []string{"new/b", "new/c"},
},
}
jsonKeyFiles, jsonStatsFiles := extractJSONFiles(jsonInfos)
assert.Equal(t, []string{"legacy/a"}, jsonKeyFiles)
assert.ElementsMatch(t, []string{"new/b", "new/c"}, jsonStatsFiles)
})
t.Run("empty input", func(t *testing.T) {
jsonKeyFiles, jsonStatsFiles := extractJSONFiles(nil)
assert.Empty(t, jsonKeyFiles)
assert.Empty(t, jsonStatsFiles)
})
}
func TestGenerateMappingsFromFiles(t *testing.T) {
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
t.Run("generate mappings for all file types", func(t *testing.T) {
files := &SegmentFiles{
InsertBinlogs: []string{"files/insert_log/111/222/333/100/log1"},
DeltaBinlogs: []string{"files/delta_log/111/222/333/100/delta1"},
StatsBinlogs: []string{"files/stats_log/111/222/333/100/stats1"},
Bm25Binlogs: []string{"files/bm25_stats/111/222/333/100/bm25_1"},
VectorScalarIndex: []string{"files/index_files/1002/1/222/333/idx1"},
TextIndex: []string{"files/text_log/123/1/111/222/333/100/text1"},
JSONKeyIndex: []string{"files/json_key_index_log/123/1/111/222/333/101/json1"},
JSONStats: []string{"files/json_stats/2/3002/1/111/222/333/102/shared_key_index/idx1"},
}
mappings, err := generateMappingsFromFiles(files, source, target)
assert.NoError(t, err)
assert.Len(t, mappings, 8)
assert.Equal(t, "files/insert_log/444/555/666/100/log1",
mappings["files/insert_log/111/222/333/100/log1"])
assert.Equal(t, "files/delta_log/444/555/666/100/delta1",
mappings["files/delta_log/111/222/333/100/delta1"])
assert.Equal(t, "files/bm25_stats/444/555/666/100/bm25_1",
mappings["files/bm25_stats/111/222/333/100/bm25_1"])
assert.Equal(t, "files/index_files/1002/1/555/666/idx1",
mappings["files/index_files/1002/1/222/333/idx1"])
})
t.Run("empty files", func(t *testing.T) {
files := &SegmentFiles{}
mappings, err := generateMappingsFromFiles(files, source, target)
assert.NoError(t, err)
assert.Empty(t, mappings)
})
t.Run("invalid path returns error", func(t *testing.T) {
files := &SegmentFiles{
InsertBinlogs: []string{"invalid/path/no/log_type"},
}
_, err := generateMappingsFromFiles(files, source, target)
assert.Error(t, err)
})
}
// TestCopySegmentAndIndexFiles_WithManifest tests the complete copy flow for StorageV3 segments.
// When storage_version >= StorageV3, insert binlogs are stored in packed format.
// Physical files (under _data/_metadata) differ from logical paths in protobuf.
// Step 3.5 must add logical pb path mappings AFTER file copying for metadata generation.
func TestCopySegmentAndIndexFiles_WithManifest(t *testing.T) {
manifestPath := packed.MarshalManifestPath("files/insert_log/111/222/333", 2)
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
StorageVersion: storage.StorageV3,
ManifestPath: manifestPath,
InsertBinlogs: []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{EntriesNum: 500, LogPath: "files/insert_log/111/222/333/100/10001", LogSize: 1024},
},
},
{
FieldID: 101,
Binlogs: []*datapb.Binlog{
{EntriesNum: 500, LogPath: "files/insert_log/111/222/333/101/10002", LogSize: 2048},
},
},
},
StatsBinlogs: []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{LogPath: "files/stats_log/111/222/333/100/20001"},
},
},
},
DeltaBinlogs: []*datapb.FieldBinlog{
{
FieldID: 0,
Binlogs: []*datapb.Binlog{
{LogPath: "files/delta_log/111/222/333/0/30001"},
},
},
},
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
t.Run("successful copy with manifest", func(t *testing.T) {
mList := mockey.Mock(listAllFiles).To(func(_ context.Context, _ storage.ChunkManager, basePath string) ([]string, error) {
return []string{
"files/insert_log/111/222/333/_data/0",
"files/insert_log/111/222/333/_metadata/manifest.json",
}, nil
}).Build()
defer mList.UnPatch()
copiedSrcPaths := make([]string, 0)
mCopy := mockey.Mock(copyFile).
To(func(_ context.Context, _ storage.ChunkManager, src, dst string) error {
copiedSrcPaths = append(copiedSrcPaths, src)
return nil
}).Build()
defer mCopy.UnPatch()
cm := &struct{ storage.ChunkManager }{}
result, copiedFiles, err := CopySegmentAndIndexFiles(context.Background(), cm, source, target, nil)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, int64(666), result.SegmentId)
assert.Equal(t, int64(1000), result.ImportedRows)
assert.Equal(t, 2, len(result.Binlogs))
assert.Equal(t, 1, len(result.Statslogs))
assert.Equal(t, 1, len(result.Deltalogs))
expectedManifestPath := packed.MarshalManifestPath("files/insert_log/444/555/666", 2)
assert.Equal(t, expectedManifestPath, result.ManifestPath)
assert.Len(t, copiedFiles, 4)
for _, src := range copiedSrcPaths {
assert.NotEqual(t, "files/insert_log/111/222/333/100/10001", src)
assert.NotEqual(t, "files/insert_log/111/222/333/101/10002", src)
}
assert.Contains(t, copiedSrcPaths, "files/insert_log/111/222/333/_data/0")
assert.Contains(t, copiedSrcPaths, "files/insert_log/111/222/333/_metadata/manifest.json")
assert.Contains(t, copiedSrcPaths, "files/stats_log/111/222/333/100/20001")
assert.Contains(t, copiedSrcPaths, "files/delta_log/111/222/333/0/30001")
})
t.Run("copy failure on physical file", func(t *testing.T) {
mList := mockey.Mock(listAllFiles).To(func(_ context.Context, _ storage.ChunkManager, basePath string) ([]string, error) {
return []string{"files/insert_log/111/222/333/_data/0"}, nil
}).Build()
defer mList.UnPatch()
mCopy := mockey.Mock(copyFile).Return(errors.New("storage unavailable")).Build()
defer mCopy.UnPatch()
cm := &struct{ storage.ChunkManager }{}
result, copiedFiles, err := CopySegmentAndIndexFiles(context.Background(), cm, source, target, nil)
assert.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "failed to copy file")
assert.Empty(t, copiedFiles)
})
t.Run("step 3.5 fails on invalid pb path format", func(t *testing.T) {
badPbSource := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
StorageVersion: storage.StorageV3,
ManifestPath: manifestPath,
InsertBinlogs: []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{EntriesNum: 500, LogPath: "invalid/path/no/log_type/10001", LogSize: 1024},
},
},
},
}
mList := mockey.Mock(listAllFiles).Return([]string{"files/insert_log/111/222/333/_data/0"}, nil).Build()
defer mList.UnPatch()
mCopy := mockey.Mock(copyFile).Return(nil).Build()
defer mCopy.UnPatch()
cm := &struct{ storage.ChunkManager }{}
result, _, err := CopySegmentAndIndexFiles(context.Background(), cm, badPbSource, target, nil)
assert.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "failed to generate target path for pb insert binlog")
})
}
// TestCopySegmentAndIndexFiles_WithManifest_NoPbBinlogs tests StorageV3 segment with no pb insert binlogs.
// When InsertBinlogs in pb is empty but manifest has files, Step 3.5 has nothing to add (no logical paths).
func TestCopySegmentAndIndexFiles_WithManifest_NoPbBinlogs(t *testing.T) {
manifestPath := packed.MarshalManifestPath("files/insert_log/111/222/333", 2)
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
StorageVersion: storage.StorageV3,
ManifestPath: manifestPath,
InsertBinlogs: []*datapb.FieldBinlog{},
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
mList := mockey.Mock(listAllFiles).Return([]string{"files/insert_log/111/222/333/_data/0"}, nil).Build()
defer mList.UnPatch()
mCopy := mockey.Mock(copyFile).Return(nil).Build()
defer mCopy.UnPatch()
cm := &struct{ storage.ChunkManager }{}
result, copiedFiles, err := CopySegmentAndIndexFiles(context.Background(), cm, source, target, nil)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, int64(0), result.ImportedRows)
assert.Len(t, copiedFiles, 1)
assert.NotEmpty(t, result.ManifestPath)
}
// TestCopySegmentAndIndexFiles_WithoutManifest_Unchanged verifies that StorageV1 segments
// use pb paths and do NOT trigger manifest-related logic (Step 3.5, Step 8).
func TestCopySegmentAndIndexFiles_WithoutManifest_Unchanged(t *testing.T) {
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
StorageVersion: storage.StorageV1,
InsertBinlogs: []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{EntriesNum: 1000, LogPath: "files/insert_log/111/222/333/100/10001", LogSize: 1024},
},
},
},
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
}
mCopy := mockey.Mock(copyFile).Return(nil).Build()
defer mCopy.UnPatch()
cm := &struct{ storage.ChunkManager }{}
result, copiedFiles, err := CopySegmentAndIndexFiles(context.Background(), cm, source, target, nil)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, int64(1000), result.ImportedRows)
assert.Equal(t, int64(666), result.SegmentId)
assert.Len(t, copiedFiles, 1)
assert.Empty(t, result.ManifestPath)
}
func TestListAllFiles(t *testing.T) {
t.Run("list files successfully", func(t *testing.T) {
mWalk := mockey.Mock((*storage.LocalChunkManager).WalkWithPrefix).
To(func(_ *storage.LocalChunkManager, _ context.Context, prefix string, recursive bool, walkFunc storage.ChunkObjectWalkFunc) error {
walkFunc(&storage.ChunkObjectInfo{FilePath: "base/path/file1"})
walkFunc(&storage.ChunkObjectInfo{FilePath: "base/path/file2"})
return nil
}).Build()
defer mWalk.UnPatch()
cm := &storage.LocalChunkManager{}
files, err := listAllFiles(context.Background(), cm, "base/path")
assert.NoError(t, err)
assert.Equal(t, []string{"base/path/file1", "base/path/file2"}, files)
})
t.Run("walk error returns error", func(t *testing.T) {
mWalk := mockey.Mock((*storage.LocalChunkManager).WalkWithPrefix).
Return(errors.New("walk failed")).Build()
defer mWalk.UnPatch()
cm := &storage.LocalChunkManager{}
files, err := listAllFiles(context.Background(), cm, "bad/path")
assert.Error(t, err)
assert.Nil(t, files)
})
}
// TestCopySegmentAndIndexFiles_V3WithTextAndJsonStats verifies that V3 segments
// with text index and JSON key stats succeed during copy. Before the fix, V3
// segments had wrong-format paths in TextIndexFiles/JsonKeyIndexFiles (etcd
// metadata), causing "no mapping found" errors during buildIndexInfoFromSource.
// The fix skips pb path extraction for V3 (files already copied via manifest
// basePath/_stats/) and passes metadata as placeholders.
func TestCopySegmentAndIndexFiles_V3WithTextAndJsonStats(t *testing.T) {
manifestPath := packed.MarshalManifestPath("files/insert_log/111/222/333", 2)
source := &datapb.CopySegmentSource{
CollectionId: 111,
PartitionId: 222,
SegmentId: 333,
StorageVersion: storage.StorageV3,
ManifestPath: manifestPath,
InsertBinlogs: []*datapb.FieldBinlog{
{
FieldID: 100,
Binlogs: []*datapb.Binlog{
{EntriesNum: 500, LogPath: "files/insert_log/111/222/333/100/10001", LogSize: 1024},
},
},
},
// Text index with relative paths (as stored in etcd after dual-write fix)
TextIndexFiles: map[int64]*datapb.TextIndexStats{
101: {
FieldID: 101,
BuildID: 7000,
Version: 1,
Files: []string{"tokenizer.json", "index.data"},
},
},
// JSON key stats with relative paths (as stored in etcd after dual-write fix)
JsonKeyIndexFiles: map[int64]*datapb.JsonKeyStats{
102: {
FieldID: 102,
BuildID: 8000,
Version: 1,
Files: []string{"shared_key_index/.managed.json_0", "shared_key_index/.managed.json_1"},
JsonKeyStatsDataFormat: 3,
},
},
}
target := &datapb.CopySegmentTarget{
CollectionId: 444,
PartitionId: 555,
SegmentId: 666,
NewBuildIds: map[int64]int64{7000: 7777, 8000: 9000},
}
mList := mockey.Mock(listAllFiles).To(func(_ context.Context, _ storage.ChunkManager, basePath string) ([]string, error) {
// Simulate listing all files under basePath including _stats/
return []string{
"files/insert_log/111/222/333/_data/0",
"files/insert_log/111/222/333/_metadata/manifest.json",
"files/insert_log/111/222/333/_stats/text_index.101/tokenizer.json",
"files/insert_log/111/222/333/_stats/text_index.101/index.data",
"files/insert_log/111/222/333/_stats/json_stats.102/shared_key_index/.managed.json_0",
"files/insert_log/111/222/333/_stats/json_stats.102/shared_key_index/.managed.json_1",
}, nil
}).Build()
defer mList.UnPatch()
mCopy := mockey.Mock(copyFile).Return(nil).Build()
defer mCopy.UnPatch()
cm := &struct{ storage.ChunkManager }{}
result, copiedFiles, err := CopySegmentAndIndexFiles(context.Background(), cm, source, target, nil)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, int64(666), result.SegmentId)
// Verify text index metadata is passed through as placeholder
assert.Len(t, result.TextIndexInfos, 1)
assert.Contains(t, result.TextIndexInfos, int64(101))
// Files are passed through as-is (placeholder, not mapped)
assert.Equal(t, []string{"tokenizer.json", "index.data"}, result.TextIndexInfos[101].GetFiles())
assert.Equal(t, int64(7777), result.TextIndexInfos[101].GetBuildID(), "text index buildID should be remapped")
// Verify JSON key stats metadata is passed through as placeholder with new buildID
assert.Len(t, result.JsonKeyIndexInfos, 1)
assert.Contains(t, result.JsonKeyIndexInfos, int64(102))
assert.Equal(t, int64(9000), result.JsonKeyIndexInfos[102].GetBuildID(), "buildID should be remapped")
// Files are passed through as-is (placeholder, not mapped)
assert.Equal(t, []string{"shared_key_index/.managed.json_0", "shared_key_index/.managed.json_1"},
result.JsonKeyIndexInfos[102].GetFiles())
// Verify manifest path is transformed
expectedManifestPath := packed.MarshalManifestPath("files/insert_log/444/555/666", 2)
assert.Equal(t, expectedManifestPath, result.ManifestPath)
// Verify _stats files are included in the copy (from listAllFiles, part of InsertBinlogs)
assert.True(t, len(copiedFiles) >= 6, "should copy all files including _stats/")
}