// Copyright 2025-present the zvec project // // Licensed 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. #include #include // NOLINT(misc-include-cleaner): protects from the private/public test macro below. #define private public #define protected public #include "db/index/segment/segment.h" #undef private #undef protected #include #include #include #include #include #include #include #include #include #include #include #include "db/common/file_helper.h" #include "db/index/common/delete_store.h" #include "db/index/common/id_map.h" #include "db/index/common/version_manager.h" #include "db/index/storage/wal/wal_file.h" #include "segment_test_fixture.h" #include "utils/utils.h" #include "zvec/db/options.h" using namespace zvec; TEST_P(SegmentTest, EmptySchema) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 0); ASSERT_TRUE(segment != nullptr); EXPECT_EQ(segment->id(), 0); segment.reset(); } TEST_P(SegmentTest, General) { options_.max_buffer_size_ = 1 * 1024; auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 25); ASSERT_TRUE(segment != nullptr); auto combined_reader = segment->scan({LOCAL_ROW_ID, "id", "name", "age"}); ASSERT_TRUE(combined_reader != nullptr); EXPECT_TRUE(combined_reader->schema() != nullptr); std::shared_ptr batch; uint32_t total_doc = 0; while (true) { auto status = combined_reader->ReadNext(&batch); if (status.ok() == false) break; if (batch == nullptr) break; EXPECT_EQ(batch->num_columns(), 4); total_doc += batch->num_rows(); } EXPECT_EQ(total_doc, 25); std::vector segment_doc_ids = {0, 3, 6, 1, 0, 14, 12, 21}; auto combined_table = segment->fetch( {LOCAL_ROW_ID, "id", "name", "age", "binary", "array_binary"}, segment_doc_ids); ASSERT_TRUE(combined_table != nullptr); EXPECT_EQ(combined_table->num_columns(), 6); EXPECT_EQ(combined_table->num_rows(), 8); auto field = combined_table->schema()->field(0); EXPECT_EQ(field->name(), LOCAL_ROW_ID); // Get data from the LOCAL_ROW_ID column for each row auto id_column = combined_table->column(0); auto id_array = std::dynamic_pointer_cast(id_column->chunk(0)); std::vector &expected_ids = segment_doc_ids; std::vector actual_ids; for (int i = 0; i < id_array->length(); ++i) { actual_ids.push_back(id_array->Value(i)); } EXPECT_EQ(actual_ids, expected_ids) << "ID column values don't match expected order"; } TEST_P(SegmentTest, InsertMoreData) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 0); ASSERT_TRUE(segment != nullptr); uint64_t MAX_DOC = 1000; auto start = std::chrono::system_clock::now(); test::TestHelper::SegmentInsertDoc(segment, *schema_, 0, MAX_DOC); auto end = std::chrono::system_clock::now(); auto cost = std::chrono::duration_cast(end - start) .count(); std::cout << "insert cost " << cost << "ms" << std::endl; auto combined_reader = segment->scan({"id", "name", "age"}); std::shared_ptr batch; uint32_t total_doc = 0; while (true) { auto status = combined_reader->ReadNext(&batch); if (status.ok() == false) break; if (batch == nullptr) break; total_doc += batch->num_rows(); } EXPECT_EQ(total_doc, MAX_DOC); } TEST_P(SegmentTest, InsertScalarTypes) { auto tmp_schema = test::TestHelper::CreateSchemaWithScalarIndex(true, true, col_name_); auto invert_params = std::make_shared(false); schema_->add_field(std::make_shared("binary", DataType::BINARY, false, invert_params)); schema_->add_field(std::make_shared( "array_binary", DataType::ARRAY_BINARY, false, invert_params)); auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 10); ASSERT_TRUE(segment != nullptr); } TEST_P(SegmentTest, InsertVectorTypes) { auto tmp_schema = test::TestHelper::CreateSchemaWithVectorIndex( false, col_name_, std::make_shared(MetricType::IP, 16, 20, QuantizeType::FP16)); // first insert 100 doc int doc_count = 100; { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *tmp_schema, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, doc_count); ASSERT_TRUE(segment != nullptr); } // Open { Version v = version_manager_->get_current_version(); auto result = Segment::Open(col_path_, *tmp_schema, *v.writing_segment_meta(), id_map_, delete_store_, version_manager_, options_); ASSERT_TRUE(result.has_value()); auto segment = result.value(); EXPECT_GT(segment->get_vector_indexer("dense_fp32").size(), 0); EXPECT_GT(segment->get_quant_vector_indexer("dense_fp32").size(), 0); } } TEST_P(SegmentTest, FetchByGlobalDocID) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 1); ASSERT_TRUE(segment != nullptr); auto ret_doc = segment->Fetch(0); EXPECT_TRUE(ret_doc != nullptr); EXPECT_EQ(ret_doc->doc_id(), 0); EXPECT_EQ(ret_doc->pk(), "pk_0"); } TEST_P(SegmentTest, FetchSingleRow) { int doc_count = 10; auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, doc_count); ASSERT_TRUE(segment != nullptr); auto func = [&](int index) -> void { ExecBatchPtr batch = segment->fetch({"id", "name", "age"}, index); ASSERT_TRUE(batch != nullptr); EXPECT_EQ(batch->length, 1); EXPECT_EQ(batch->values.size(), 3); auto id_scalar = batch->values[0].scalar(); ASSERT_TRUE(id_scalar != nullptr); auto id_value = std::dynamic_pointer_cast(id_scalar); ASSERT_TRUE(id_value != nullptr); EXPECT_EQ(id_value->value, index); }; for (int i = 0; i < doc_count; ++i) { func(i); } } TEST_P(SegmentTest, FetchSingleRowWithPersistStore) { // first insert 1000 doc int doc_count = 1000; { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, doc_count); ASSERT_TRUE(segment != nullptr); } // Open { Version v = version_manager_->get_current_version(); SegmentOptions open_options; open_options.read_only_ = false; auto result = Segment::Open(col_path_, *schema_, *v.writing_segment_meta(), id_map_, delete_store_, version_manager_, open_options); ASSERT_TRUE(result.has_value()); auto segment = result.value(); test::TestHelper::SegmentInsertDoc(segment, *schema_, doc_count, doc_count * 2); auto func = [&](int index) -> void { ExecBatchPtr batch = segment->fetch({"id", "name", "age"}, index); ASSERT_TRUE(batch != nullptr); EXPECT_EQ(batch->length, 1); EXPECT_EQ(batch->values.size(), 3); auto id_scalar = batch->values[0].scalar(); ASSERT_TRUE(id_scalar != nullptr); auto id_value = std::dynamic_pointer_cast(id_scalar); ASSERT_TRUE(id_value != nullptr); EXPECT_EQ(id_value->value, index); }; for (int i = 0; i < doc_count * 2; ++i) { func(i); } } } TEST_P(SegmentTest, FetchSingleRowWithUserID) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 10); ASSERT_TRUE(segment != nullptr); ExecBatchPtr batch = segment->fetch({USER_ID, "id", "name"}, 2); ASSERT_TRUE(batch != nullptr); EXPECT_EQ(batch->length, 1); EXPECT_EQ(batch->values.size(), 3); auto user_id_scalar = batch->values[0].scalar(); ASSERT_TRUE(user_id_scalar != nullptr); EXPECT_TRUE(std::dynamic_pointer_cast(user_id_scalar) != nullptr); } TEST_P(SegmentTest, FetchSingleRowWithGlobalDocID) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 10); ASSERT_TRUE(segment != nullptr); ExecBatchPtr batch = segment->fetch({GLOBAL_DOC_ID, "id", "name"}, 4); ASSERT_TRUE(batch != nullptr); EXPECT_EQ(batch->length, 1); EXPECT_EQ(batch->values.size(), 3); auto global_doc_id_scalar = batch->values[0].scalar(); ASSERT_TRUE(global_doc_id_scalar != nullptr); EXPECT_TRUE(std::dynamic_pointer_cast( global_doc_id_scalar) != nullptr); } TEST_P(SegmentTest, FetchSingleRowWithNegativeIndex) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 10); ASSERT_TRUE(segment != nullptr); ExecBatchPtr batch = segment->fetch({"id", "name"}, -1); EXPECT_EQ(batch, nullptr); } TEST_P(SegmentTest, FetchSingleRowWithOutOfRangeIndex) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 10); ASSERT_TRUE(segment != nullptr); ExecBatchPtr batch = segment->fetch({"id", "name"}, 15); EXPECT_EQ(batch, nullptr); } TEST_P(SegmentTest, FetchSingleRowWithInvalidColumn) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 10); ASSERT_TRUE(segment != nullptr); ExecBatchPtr batch = segment->fetch({"id", "invalid_column"}, 0); EXPECT_EQ(batch, nullptr); } TEST_P(SegmentTest, FetchSingleRowWithEmptyColumns) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 10); ASSERT_TRUE(segment != nullptr); ExecBatchPtr batch = segment->fetch({}, 0); EXPECT_EQ(batch, nullptr); } TEST_P(SegmentTest, FetchSingleRowFromEmptySegment) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 0); ASSERT_TRUE(segment != nullptr); ExecBatchPtr batch = segment->fetch({"id", "name"}, 0); EXPECT_EQ(batch, nullptr); } TEST_P(SegmentTest, FetchSingleRowWithBinaryFields) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 10); ASSERT_TRUE(segment != nullptr); ExecBatchPtr batch = segment->fetch({"binary", "array_binary"}, 1); ASSERT_TRUE(batch != nullptr); EXPECT_EQ(batch->length, 1); EXPECT_EQ(batch->values.size(), 2); auto binary_scalar = batch->values[0].scalar(); ASSERT_TRUE(binary_scalar != nullptr); EXPECT_TRUE(std::dynamic_pointer_cast(binary_scalar) != nullptr); auto array_binary_scalar = batch->values[1].scalar(); ASSERT_TRUE(array_binary_scalar != nullptr); EXPECT_TRUE(std::dynamic_pointer_cast( array_binary_scalar) != nullptr); } TEST_P(SegmentTest, Recover) { // first insert 100 doc int doc_count = 100; { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, doc_count); ASSERT_TRUE(segment != nullptr); } // simulate wal file { Version v = version_manager_->get_current_version(); auto writing_block_id = v.writing_segment_meta()->writing_forward_block_->id(); auto wal_file = FileHelper::MakeWalPath(col_path_, 0, writing_block_id); WalOptions wal_option{0, true}; WalFilePtr wal_file_; WalFile::CreateAndOpen(wal_file, wal_option, &wal_file_); ASSERT_TRUE(wal_file_ != nullptr); for (int i = doc_count; i < doc_count + 100; i++) { Doc doc = test::TestHelper::CreateDoc(i, *schema_); doc.set_operator(Operator::INSERT); std::vector buf = doc.serialize(); auto ret = wal_file_->append(std::string(buf.begin(), buf.end())); ASSERT_EQ(ret, 0); } for (int i = 0; i < doc_count; i++) { Doc doc = test::TestHelper::CreateDoc(i, *schema_); doc.set_doc_id(i); // global doc id doc.set_operator(Operator::UPDATE); std::vector buf = doc.serialize(); auto ret = wal_file_->append(std::string(buf.begin(), buf.end())); ASSERT_EQ(ret, 0); } for (int i = 0; i < doc_count; i++) { Doc doc = test::TestHelper::CreateDoc(i, *schema_); doc.set_operator(Operator::UPSERT); std::vector buf = doc.serialize(); auto ret = wal_file_->append(std::string(buf.begin(), buf.end())); ASSERT_EQ(ret, 0); } for (int i = 0; i < doc_count; i++) { Doc doc = test::TestHelper::CreateDoc(i, *schema_); doc.set_doc_id(i + 300); // global doc id doc.set_operator(Operator::DELETE); std::vector buf = doc.serialize(); auto ret = wal_file_->append(std::string(buf.begin(), buf.end())); ASSERT_EQ(ret, 0); } } // recover { Version v = version_manager_->get_current_version(); SegmentOptions open_options; open_options.read_only_ = false; auto result = Segment::Open(col_path_, *schema_, *v.writing_segment_meta(), id_map_, delete_store_, version_manager_, open_options); ASSERT_TRUE(result.has_value()); auto segment = result.value(); auto combined_reader = segment->scan({"id"}); std::shared_ptr batch; uint32_t total_doc = 0; while (true) { auto status = combined_reader->ReadNext(&batch); if (status.ok() == false) break; if (batch == nullptr) break; total_doc += batch->num_rows(); EXPECT_EQ(batch->num_columns(), 1); } // Why 400 ? because in segment we just mark deleted doc EXPECT_EQ(total_doc, 400); auto filter = delete_store_->make_filter(); auto actual_doc_count = segment->doc_count(filter); EXPECT_EQ(actual_doc_count, 100); } } TEST_P(SegmentTest, UpdateDoc) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 10); ASSERT_TRUE(segment != nullptr); // before update uint64_t count = segment->doc_count(delete_store_->make_filter()); EXPECT_EQ(count, 10); // Create a new document to update Doc update_doc = test::TestHelper::CreateDoc(5, *schema_); update_doc.set("name", "updated_name"); update_doc.set("age", 99); // Update the document auto status = segment->Update(update_doc); EXPECT_TRUE(status.ok()) << "Update failed: " << status.message(); // after update count = segment->doc_count(delete_store_->make_filter()); EXPECT_EQ(count, 10); // Fetch the updated document and verify changes // Note: The parameter here is the internal global_doc_id, not user-specified auto ret_doc = segment->Fetch(10); EXPECT_TRUE(ret_doc != nullptr); EXPECT_EQ(ret_doc->get("name"), "updated_name"); EXPECT_EQ(ret_doc->get("age"), 99); } TEST_P(SegmentTest, UpdateDocBatch) { int doc_count = 10; auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, doc_count); ASSERT_TRUE(segment != nullptr); // before update uint64_t count = segment->doc_count(delete_store_->make_filter()); EXPECT_EQ(count, doc_count); // Create a new document to update for (int i = 0; i < doc_count; i++) { Doc update_doc = test::TestHelper::CreateDoc(i, *schema_); // Update the document auto status = segment->Update(update_doc); EXPECT_TRUE(status.ok()) << "Update failed: " << status.message(); } // after update count = segment->doc_count(delete_store_->make_filter()); EXPECT_EQ(count, doc_count); // Fetch the updated document and verify changes // Note: The parameter here is the internal global_doc_id, not user-specified auto ret_doc = segment->Fetch(doc_count * 2 - 1); EXPECT_TRUE(ret_doc != nullptr); EXPECT_EQ(ret_doc->get("name"), "value_" + std::to_string(doc_count - 1)); } TEST_P(SegmentTest, DeleteDoc) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 10); ASSERT_TRUE(segment != nullptr); // before update uint64_t count = segment->doc_count(delete_store_->make_filter()); EXPECT_EQ(count, 10); // Delete a document by primary key auto status = segment->Delete("pk_5"); EXPECT_TRUE(status.ok()) << "Delete by pk failed: " << status.message(); // after delete count = segment->doc_count(delete_store_->make_filter()); EXPECT_EQ(count, 9); // Delete a document by global doc id status = segment->Delete(3); EXPECT_TRUE(status.ok()) << "Delete by global doc id failed: " << status.message(); count = segment->doc_count(delete_store_->make_filter()); EXPECT_EQ(count, 8); } TEST_P(SegmentTest, DeleteBatch) { int doc_count = 10; auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, doc_count); ASSERT_TRUE(segment != nullptr); // before update uint64_t count = segment->doc_count(delete_store_->make_filter()); EXPECT_EQ(count, doc_count); for (int i = 0; i < doc_count; i++) { auto status = segment->Delete("pk_" + std::to_string(i)); EXPECT_TRUE(status.ok()) << "Delete by pk failed: " << status.message(); } // after delete count = segment->doc_count(delete_store_->make_filter()); EXPECT_EQ(count, 0); } TEST_P(SegmentTest, UpsertDoc) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 5); ASSERT_TRUE(segment != nullptr); // before update uint64_t count = segment->doc_count(delete_store_->make_filter()); EXPECT_EQ(count, 5); // Upsert an existing document Doc upsert_doc1 = test::TestHelper::CreateDoc(3, *schema_); upsert_doc1.set("name", "upserted_name"); auto status = segment->Upsert(upsert_doc1); EXPECT_TRUE(status.ok()) << "Upsert existing doc failed: " << status.message(); count = segment->doc_count(delete_store_->make_filter()); EXPECT_EQ(count, 5); // Verify the update auto ret_doc = segment->Fetch(5); EXPECT_TRUE(ret_doc != nullptr); EXPECT_EQ(ret_doc->get("name"), "upserted_name"); // Upsert a new document Doc upsert_doc2 = test::TestHelper::CreateDoc(6, *schema_); upsert_doc2.set("name", "new_upserted_doc"); status = segment->Upsert(upsert_doc2); EXPECT_TRUE(status.ok()) << "Upsert new doc failed: " << status.message(); count = segment->doc_count(delete_store_->make_filter()); EXPECT_EQ(count, 6); // Verify the new document was inserted ret_doc = segment->Fetch(6); EXPECT_TRUE(ret_doc != nullptr); EXPECT_EQ(ret_doc->get("name"), "new_upserted_doc"); } TEST_P(SegmentTest, UpsertDocBatch) { int doc_count = 10; auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, doc_count); ASSERT_TRUE(segment != nullptr); // before update uint64_t count = segment->doc_count(delete_store_->make_filter()); EXPECT_EQ(count, doc_count); for (int i = 0; i < doc_count; i++) { // Upsert existing document Doc upsert_doc1 = test::TestHelper::CreateDoc(i, *schema_); upsert_doc1.set("name", "upserted_name" + std::to_string(i)); auto status = segment->Upsert(upsert_doc1); EXPECT_TRUE(status.ok()) << "Upsert existing doc failed: " << status.message(); // Upsert new document Doc upsert_doc2 = test::TestHelper::CreateDoc(doc_count + i, *schema_); upsert_doc2.set("name", "new_upserted_doc" + std::to_string(i)); status = segment->Upsert(upsert_doc2); EXPECT_TRUE(status.ok()) << "Upsert new doc failed: " << status.message(); } count = segment->doc_count(delete_store_->make_filter()); EXPECT_EQ(count, doc_count * 2); int incr_idx = 0; for (int i = doc_count; i < doc_count + doc_count * 2; i += 2) { // Verify the update auto ret_doc = segment->Fetch(i); EXPECT_TRUE(ret_doc != nullptr); EXPECT_EQ(ret_doc->get("name"), "upserted_name" + std::to_string(incr_idx)); // Verify the new document was inserted ret_doc = segment->Fetch(i + 1); EXPECT_EQ(ret_doc->get("name"), "new_upserted_doc" + std::to_string(incr_idx)); incr_idx++; } } TEST_P(SegmentTest, Flush) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 100); ASSERT_TRUE(segment != nullptr); // Flush the segment auto status = segment->flush(); EXPECT_TRUE(status.ok()) << "Flush failed: " << status.message(); } TEST_P(SegmentTest, FlushAfterInsert) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 100); ASSERT_TRUE(segment != nullptr); // Flush the segment auto status = segment->flush(); EXPECT_TRUE(status.ok()) << "Flush failed: " << status.message(); test::TestHelper::SegmentInsertDoc(segment, *schema_, 100, 150); ASSERT_EQ(segment->doc_count(), 150); for (int i = 0; i < 150; i++) { auto ret_doc = segment->Fetch(i); EXPECT_TRUE(ret_doc != nullptr); Doc verify_doc = test::TestHelper::CreateDoc(i, *schema_); auto vv = verify_doc.get>("dense_fp32").value(); auto v = ret_doc->get>("dense_fp32").value(); for (uint32_t j = 0; j < vv.size(); j++) { ASSERT_FLOAT_EQ(v[j], vv[j]); } } } TEST_P(SegmentTest, Dump) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 100); ASSERT_TRUE(segment != nullptr); // Dump the segment auto status = segment->dump(); EXPECT_TRUE(status.ok()) << "Flush failed: " << status.message(); status = segment->dump(); EXPECT_FALSE(status.ok()); EXPECT_EQ(status.code(), StatusCode::NOT_SUPPORTED); } TEST_P(SegmentTest, DocCount) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 50); ASSERT_TRUE(segment != nullptr); // Get document count uint64_t count = segment->doc_count(); EXPECT_EQ(count, 50); // Delete some documents segment->Delete("pk_10"); segment->Delete("pk_20"); segment->Delete("pk_30"); // Get document count again count = segment->doc_count(delete_store_->make_filter()); EXPECT_EQ(count, 47); } // TEST_P(SegmentTest, Insert100WData) { // options_.max_buffer_size_ = 8 * 1024 * 1024; // auto segment = test::TestHelper::CreateSegmentWithDoc( // col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, // options_, 0, 0); // ASSERT_TRUE(segment != nullptr); // uint64_t MAX_DOC = 1000000; // auto start = std::chrono::system_clock::now(); // test::TestHelper::SegmentInsertDoc(segment, *schema_, 0, MAX_DOC); // auto end = std::chrono::system_clock::now(); // auto cost = std::chrono::duration_cast(end - // start) // .count(); // std::cout << "insert cost " << cost << "ms" << std::endl; // start = std::chrono::system_clock::now(); // ; // auto combined_reader = segment->scan( // {"id", "name", "age", USER_ID, GLOBAL_DOC_ID, LOCAL_ROW_ID}); // std::shared_ptr batch; // uint32_t total_doc = 0; // while (true) { // auto status = combined_reader->ReadNext(&batch); // if (status.ok() == false) break; // if (batch == nullptr) break; // total_doc += batch->num_rows(); // } // end = std::chrono::system_clock::now(); // cost = std::chrono::duration_cast(end - start) // .count(); // std::cout << "scan cost " << cost << "ms" << std::endl; // EXPECT_EQ(total_doc, MAX_DOC); // } TEST_P(SegmentTest, CombinedVectorColumnIndexer) { options_.max_buffer_size_ = 10 * 1024; auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 0); ASSERT_TRUE(segment != nullptr); uint64_t MAX_DOC = 1000; test::TestHelper::SegmentInsertDoc(segment, *schema_, 0, MAX_DOC); Doc new_doc = test::TestHelper::CreateDoc(1000, *schema_); auto status = segment->Insert(new_doc); ASSERT_TRUE(status.ok()); auto combined_indexer = segment->get_combined_vector_indexer("dense_fp32"); ASSERT_TRUE(combined_indexer != nullptr); // fetch auto fetched_data = combined_indexer->Fetch(1000); ASSERT_TRUE(fetched_data); const float *dense_vector = reinterpret_cast( std::get( fetched_data->vector_buffer) .data.data()); auto vv = new_doc.get>("dense_fp32").value(); for (uint32_t i = 0; i < vv.size(); i++) { ASSERT_FLOAT_EQ(dense_vector[i], vv[i]); } // query auto dense_fp32_field = schema_->get_field("dense_fp32"); auto query_vector = new_doc.get>("dense_fp32").value(); auto query = vector_column_params::VectorData{ vector_column_params::DenseVector{query_vector.data()}}; vector_column_params::QueryParams query_params; query_params.dimension = dense_fp32_field->dimension(); query_params.topk = 10; query_params.filter = nullptr; query_params.fetch_vector = false; auto results = combined_indexer->Search(query, query_params); ASSERT_TRUE(results.has_value()); auto vector_results = dynamic_cast(results.value().get()); ASSERT_TRUE(vector_results); ASSERT_EQ(vector_results->count(), 10); int count = 0; auto iter = vector_results->create_iterator(); while (iter->valid()) { count++; iter->next(); } ASSERT_EQ(count, 10); } TEST_P(SegmentTest, CombinedVectorColumnIndexerWithQuantVectorIndex) { options_.max_buffer_size_ = 10 * 1024; auto tmp_schema = test::TestHelper::CreateSchemaWithVectorIndex( false, "demo", std::make_shared(MetricType::IP, 16, 20, QuantizeType::FP16)); auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *tmp_schema, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 0); ASSERT_TRUE(segment != nullptr); uint64_t MAX_DOC = 1000; test::TestHelper::SegmentInsertDoc(segment, *schema_, 0, MAX_DOC); Doc new_doc = test::TestHelper::CreateDoc(1000, *schema_); auto status = segment->Insert(new_doc); ASSERT_TRUE(status.ok()); auto combined_indexer = segment->get_quant_combined_vector_indexer("dense_fp32"); ASSERT_TRUE(combined_indexer != nullptr); // fetch auto fetched_data = combined_indexer->Fetch(1000); ASSERT_TRUE(fetched_data); const float *dense_vector = reinterpret_cast( std::get( fetched_data->vector_buffer) .data.data()); auto vv = new_doc.get>("dense_fp32").value(); for (uint32_t i = 0; i < vv.size(); i++) { EXPECT_NEAR(dense_vector[i], vv[i], 0.1); } // query auto dense_fp32_field = schema_->get_field("dense_fp32"); auto query_vector = new_doc.get>("dense_fp32").value(); auto query = vector_column_params::VectorData{ vector_column_params::DenseVector{query_vector.data()}}; vector_column_params::QueryParams query_params; query_params.dimension = dense_fp32_field->dimension(); query_params.topk = 10; query_params.filter = nullptr; query_params.fetch_vector = false; query_params.query_params = std::make_shared(IndexType::HNSW); query_params.query_params->set_is_using_refiner(true); auto results = combined_indexer->Search(query, query_params); ASSERT_TRUE(results.has_value()); auto vector_results = dynamic_cast(results.value().get()); ASSERT_TRUE(vector_results); ASSERT_EQ(vector_results->count(), 10); int count = 0; auto iter = vector_results->create_iterator(); while (iter->valid()) { count++; iter->next(); } ASSERT_EQ(count, 10); } TEST_P(SegmentTest, CombinedVectorColumnIndexerQueryWithPks) { options_.max_buffer_size_ = 10 * 1024; auto tmp_schema = test::TestHelper::CreateSchemaWithVectorIndex( false, "demo", std::make_shared(MetricType::IP)); auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *tmp_schema, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 0); ASSERT_TRUE(segment != nullptr); uint64_t MAX_DOC = 1000; test::TestHelper::SegmentInsertDoc(segment, *schema_, 0, MAX_DOC); auto combined_indexer = segment->get_combined_vector_indexer("dense_fp32"); ASSERT_TRUE(combined_indexer != nullptr); Doc verify_doc = test::TestHelper::CreateDoc(999, *schema_); std::vector> bf_pks = { {10, 20, 30, 40, 50, 60, 70, 80, 90, 999}}; // query auto dense_fp32_field = schema_->get_field("dense_fp32"); auto query_vector = verify_doc.get>("dense_fp32").value(); auto query = vector_column_params::VectorData{ vector_column_params::DenseVector{query_vector.data()}}; vector_column_params::QueryParams query_params; query_params.data_type = dense_fp32_field->data_type(); query_params.dimension = dense_fp32_field->dimension(); query_params.topk = 10; query_params.filter = nullptr; query_params.fetch_vector = false; query_params.query_params = std::make_shared(IndexType::HNSW); query_params.bf_pks = bf_pks; auto results = combined_indexer->Search(query, query_params); ASSERT_TRUE(results.has_value()); auto vector_results = dynamic_cast(results.value().get()); ASSERT_TRUE(vector_results); ASSERT_EQ(vector_results->count(), 10); int count = 0; std::vector result_doc_ids; auto iter = vector_results->create_iterator(); while (iter->valid()) { count++; result_doc_ids.push_back(iter->doc_id()); iter->next(); } ASSERT_EQ(count, 10); // need reverse result_doc_ids std::reverse(result_doc_ids.begin(), result_doc_ids.end()); ASSERT_EQ(result_doc_ids, bf_pks[0]); } TEST_P(SegmentTest, ConcurrentInsertOperations) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 0); ASSERT_TRUE(segment != nullptr); const int num_threads = 4; const int docs_per_thread = 50; std::vector threads; // Launch multiple threads to insert documents concurrently for (int t = 0; t < num_threads; ++t) { threads.emplace_back([&, t]() { for (int i = 0; i < docs_per_thread; ++i) { int doc_id = t * docs_per_thread + i; Doc doc = test::TestHelper::CreateDoc(doc_id, *schema_); auto status = segment->Insert(doc); EXPECT_TRUE(status.ok()) << "Thread " << t << " insert failed for doc " << doc_id; } }); } // Wait for all threads to complete for (auto &thread : threads) { thread.join(); } // Verify total document count uint64_t count = segment->doc_count(); EXPECT_EQ(count, num_threads * docs_per_thread); } TEST_P(SegmentTest, ConcurrentMixedOperations) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 100); ASSERT_TRUE(segment != nullptr); std::vector threads; // Thread 1: Insert new documents threads.emplace_back([&]() { for (int i = 100; i < 120; ++i) { Doc doc = test::TestHelper::CreateDoc(i, *schema_); auto status = segment->Insert(doc); EXPECT_TRUE(status.ok() || status.code() == StatusCode::ALREADY_EXISTS); } }); // Thread 2: Update existing documents threads.emplace_back([&]() { for (int i = 0; i < 50; i += 5) { Doc doc = test::TestHelper::CreateDoc(i, *schema_); doc.set("name", "updated_concurrent_" + std::to_string(i)); auto status = segment->Update(doc); EXPECT_TRUE(status.ok() || status.code() == StatusCode::NOT_FOUND); } }); // Thread 3: Delete documents threads.emplace_back([&]() { for (int i = 50; i < 100; i += 10) { auto status = segment->Delete("pk_" + std::to_string(i)); EXPECT_TRUE(status.ok() || status.code() == StatusCode::NOT_FOUND); } }); // Wait for all threads to complete for (auto &thread : threads) { thread.join(); } } // corner cases TEST_P(SegmentTest, DuplicateInsert) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 0); ASSERT_TRUE(segment != nullptr); Doc doc1 = test::TestHelper::CreateDoc(0, *schema_); auto status1 = segment->Insert(doc1); EXPECT_TRUE(status1.ok()) << "First insert failed: " << status1.message(); auto meta = segment->meta(); ASSERT_TRUE(meta != nullptr); auto &mem_block = meta->writing_forward_block().value(); EXPECT_EQ(mem_block.doc_count_, 1); EXPECT_EQ(mem_block.min_doc_id_, 0); EXPECT_EQ(mem_block.max_doc_id_, 0); auto doc = segment->Fetch(0); EXPECT_TRUE(doc != nullptr); EXPECT_EQ(*doc, doc1); auto status2 = segment->Insert(doc1); EXPECT_FALSE(status2.ok()) << "Duplicate insert should fail"; auto fetched_doc = segment->Fetch(0); ASSERT_TRUE(fetched_doc != nullptr); EXPECT_NE(fetched_doc->get("name").value(), "duplicate_name"); } TEST_P(SegmentTest, DuplicateDelete) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 5); ASSERT_TRUE(segment != nullptr); auto status1 = segment->Delete("pk_2"); EXPECT_TRUE(status1.ok()) << "First delete failed: " << status1.message(); auto status2 = segment->Delete("pk_2"); EXPECT_FALSE(status2.ok()) << "Duplicate delete should fail"; auto status3 = segment->Delete(2); EXPECT_FALSE(status3.ok()) << "Delete by doc_id of already deleted doc should fail"; } TEST_P(SegmentTest, DeleteNonExistentDoc) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 5); ASSERT_TRUE(segment != nullptr); auto status1 = segment->Delete("pk_999"); EXPECT_FALSE(status1.ok()) << "Delete non-existent pk should fail"; } TEST_P(SegmentTest, UpdateNonExistentDoc) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 5); ASSERT_TRUE(segment != nullptr); Doc doc = test::TestHelper::CreateDoc(999, *schema_); doc.set("name", "non_existent_doc"); auto status = segment->Update(doc); EXPECT_FALSE(status.ok()) << "Update non-existent doc should fail"; } TEST_P(SegmentTest, UpsertNonExistentDoc) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 5); ASSERT_TRUE(segment != nullptr); Doc doc = test::TestHelper::CreateDoc(999, *schema_); doc.set("name", "new_upserted_doc"); auto status = segment->Upsert(doc); EXPECT_TRUE(status.ok()) << "Upsert non-existent doc should succeed: " << status.message(); auto filter = delete_store_->make_filter(); uint64_t count = segment->doc_count(filter); EXPECT_EQ(count, 6); } TEST_P(SegmentTest, ScanWithEmptyColumns) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 5); ASSERT_TRUE(segment != nullptr); auto reader = segment->scan({}); ASSERT_TRUE(reader == nullptr); } TEST_P(SegmentTest, ScanWithInvalidColumns) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 10); ASSERT_TRUE(segment != nullptr); // Try to scan with invalid column name auto reader = segment->scan({"invalid_column"}); EXPECT_TRUE(reader == nullptr); } TEST_P(SegmentTest, FetchNonExistentDoc) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 5); ASSERT_TRUE(segment != nullptr); auto doc = segment->Fetch(999); EXPECT_TRUE(doc == nullptr) << "Fetch non-existent doc should return nullptr"; } TEST_P(SegmentTest, FetchWithInvalidSegmentDocIDs) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 5); ASSERT_TRUE(segment != nullptr); std::vector invalid_segment_doc_ids = {999, 1000}; auto table = segment->fetch({"id", "name"}, invalid_segment_doc_ids); ASSERT_TRUE(table == nullptr); } TEST_P(SegmentTest, FetchWithInvalidColumns) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 10); ASSERT_TRUE(segment != nullptr); // Try to fetch with invalid column name std::vector segment_doc_ids = {0, 1, 2}; auto table = segment->fetch({"invalid_column"}, segment_doc_ids); EXPECT_TRUE(table == nullptr); } TEST_P(SegmentTest, InsertEmptyDocWithNullableSchema) { auto nullable_schema = test::TestHelper::CreateNormalSchema(true, col_name_); auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *nullable_schema, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 0); ASSERT_TRUE(segment != nullptr); Doc empty_doc; empty_doc.set_pk("pk_empty"); auto status = segment->Insert(empty_doc); EXPECT_TRUE(status.ok()); } TEST_P(SegmentTest, MultipleDuplicateDeletes) { auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, 5); ASSERT_TRUE(segment != nullptr); auto status1 = segment->Delete("pk_1"); EXPECT_TRUE(status1.ok()); for (int i = 0; i < 10; ++i) { auto status = segment->Delete("pk_1"); EXPECT_FALSE(status.ok()) << "Delete iteration " << i << " should fail"; } auto filter = delete_store_->make_filter(); uint64_t count = segment->doc_count(filter); EXPECT_EQ(count, 4); } TEST_P(SegmentTest, FetchWithTwoVectorFields) { schema_->add_field(std::make_shared( "dense2_fp32", DataType::VECTOR_FP32, 128, false, std::make_shared(MetricType::IP))); int doc_count = 1000; auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, doc_count); ASSERT_TRUE(segment != nullptr); segment.reset(); version_manager_.reset(); id_map_->flush(); id_map_.reset(); std::string delete_store_path = FileHelper::MakeFilePath(col_path_, FileID::DELETE_FILE, 0); delete_store_->flush(delete_store_path); delete_store_.reset(); auto recover_version_manager = VersionManager::Recovery(col_path_); auto recover_version_mgr = recover_version_manager.value(); ASSERT_TRUE(recover_version_mgr != nullptr); auto v = recover_version_mgr->get_current_version(); // idmap std::string idmap_path = FileHelper::MakeFilePath(col_path_, FileID::ID_FILE, v.id_map_path_suffix()); IDMap::Ptr recover_id_map = std::make_shared(col_name_); auto status = recover_id_map->open(idmap_path, false, false); ASSERT_TRUE(status.ok()); delete_store_path = FileHelper::MakeFilePath(col_path_, FileID::DELETE_FILE, v.delete_snapshot_path_suffix()); auto recover_delete_store = DeleteStore::CreateAndLoad(col_name_, delete_store_path); ASSERT_TRUE(recover_delete_store != nullptr); int incr_doc_count = 1000; auto result = Segment::Open(col_path_, *schema_, *v.writing_segment_meta(), recover_id_map, recover_delete_store, recover_version_mgr, options_); ASSERT_TRUE(result.has_value()); segment = std::move(result).value(); ASSERT_TRUE(segment != nullptr); auto s = test::TestHelper::SegmentInsertDoc( segment, *schema_, doc_count, doc_count + incr_doc_count, false); ASSERT_TRUE(s.ok()); for (int i = 0; i < doc_count + incr_doc_count; i++) { auto expect_doc = test::TestHelper::CreateDoc(i, *schema_); auto ret_doc = segment->Fetch(i); if (*ret_doc != expect_doc) { std::cout << " ret_doc: " << ret_doc->to_string() << std::endl; std::cout << "expect_doc: " << expect_doc.to_string() << std::endl; } ASSERT_EQ(*ret_doc, expect_doc); } } TEST_P(SegmentTest, FetchPerf) { // create segment int doc_count = 1000; options_.max_buffer_size_ = 100 * 1024; auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, doc_count); ASSERT_TRUE(segment != nullptr); segment->dump(); auto writing_segment_meta = segment->meta(); // convert writing segment meta to persisted segment meta Version version = version_manager_->get_current_version(); writing_segment_meta->remove_writing_forward_block(); auto s = version.add_persisted_segment_meta(writing_segment_meta); ASSERT_TRUE(s.ok()); s = version_manager_->apply(version); ASSERT_TRUE(s.ok()); s = version_manager_->flush(); ASSERT_TRUE(s.ok()); segment.reset(); version_manager_.reset(); id_map_->flush(); id_map_.reset(); std::string delete_store_path = FileHelper::MakeFilePath(col_path_, FileID::DELETE_FILE, 0); delete_store_->flush(delete_store_path); delete_store_.reset(); auto recover_version_manager = VersionManager::Recovery(col_path_); auto recover_version_mgr = recover_version_manager.value(); ASSERT_TRUE(recover_version_mgr != nullptr); Version v = recover_version_mgr->get_current_version(); const auto &persist_metas = v.persisted_segment_metas(); // idmap std::string idmap_path = FileHelper::MakeFilePath(col_path_, FileID::ID_FILE, v.id_map_path_suffix()); IDMap::Ptr recover_id_map = std::make_shared(col_name_); auto status = recover_id_map->open(idmap_path, false, false); ASSERT_TRUE(status.ok()); delete_store_path = FileHelper::MakeFilePath(col_path_, FileID::DELETE_FILE, v.delete_snapshot_path_suffix()); auto recover_delete_store = DeleteStore::CreateAndLoad(col_name_, delete_store_path); ASSERT_TRUE(recover_delete_store != nullptr); // open persist segment options_.read_only_ = true; auto result = Segment::Open(col_path_, *schema_, *persist_metas[0], recover_id_map, recover_delete_store, recover_version_mgr, options_); ASSERT_TRUE(result.has_value()); segment = std::move(result).value(); ASSERT_TRUE(segment != nullptr); s = segment->add_column( std::make_shared("add_int32", DataType::INT32, false), "int32 + 1", AddColumnOptions()); EXPECT_TRUE(s.ok()); std::vector segment_doc_ids = {0, 3, 6, 1, 0, 501, 999}; auto func = [&](const std::vector columns, int local_row_id_idx) -> void { auto combined_table = segment->fetch(columns, segment_doc_ids); ASSERT_TRUE(combined_table != nullptr); EXPECT_EQ(combined_table->num_columns(), columns.size()); EXPECT_EQ(combined_table->num_rows(), segment_doc_ids.size()); auto field = combined_table->schema()->field(local_row_id_idx); EXPECT_EQ(field->name(), LOCAL_ROW_ID); // Get data from the LOCAL_ROW_ID column for each row auto id_column = combined_table->column(local_row_id_idx); auto id_array = std::dynamic_pointer_cast(id_column->chunk(0)); std::vector &expected_ids = segment_doc_ids; std::vector actual_ids; for (int i = 0; i < id_array->length(); ++i) { actual_ids.push_back(id_array->Value(i)); } EXPECT_EQ(actual_ids, expected_ids) << "ID column values don't match expected order"; }; func({LOCAL_ROW_ID, "id", "name", "add_int32"}, 0); func( { "id", LOCAL_ROW_ID, "name", "add_int32", }, 1); func({"id", "name", "add_int32", LOCAL_ROW_ID}, 3); } TEST_P(SegmentTest, AddColumn) { // create segment options_.max_buffer_size_ = 10 * 1024 * 1024; int doc_count = 1000; auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, doc_count); ASSERT_TRUE(segment != nullptr); auto s = segment->add_column( std::make_shared("add_int32", DataType::INT32, false), "int32 + 1", AddColumnOptions()); EXPECT_FALSE(s.ok()); segment->dump(); auto writing_segment_meta = segment->meta(); // convert writing segment meta to persisted segment meta Version version = version_manager_->get_current_version(); writing_segment_meta->remove_writing_forward_block(); s = version.add_persisted_segment_meta(writing_segment_meta); ASSERT_TRUE(s.ok()); s = version_manager_->apply(version); ASSERT_TRUE(s.ok()); s = version_manager_->flush(); ASSERT_TRUE(s.ok()); segment.reset(); version_manager_.reset(); id_map_->flush(); id_map_.reset(); std::string delete_store_path = FileHelper::MakeFilePath(col_path_, FileID::DELETE_FILE, 0); delete_store_->flush(delete_store_path); delete_store_.reset(); auto recover_version_manager = VersionManager::Recovery(col_path_); auto recover_version_mgr = recover_version_manager.value(); ASSERT_TRUE(recover_version_mgr != nullptr); Version v = recover_version_mgr->get_current_version(); const auto &persist_metas = v.persisted_segment_metas(); // idmap std::string idmap_path = FileHelper::MakeFilePath(col_path_, FileID::ID_FILE, v.id_map_path_suffix()); IDMap::Ptr recover_id_map = std::make_shared(col_name_); auto status = recover_id_map->open(idmap_path, false, false); ASSERT_TRUE(status.ok()); delete_store_path = FileHelper::MakeFilePath(col_path_, FileID::DELETE_FILE, v.delete_snapshot_path_suffix()); auto recover_delete_store = DeleteStore::CreateAndLoad(col_name_, delete_store_path); ASSERT_TRUE(recover_delete_store != nullptr); // open persist segment options_.read_only_ = true; auto result = Segment::Open(col_path_, *schema_, *persist_metas[0], recover_id_map, recover_delete_store, recover_version_mgr, options_); ASSERT_TRUE(result.has_value()); segment = std::move(result).value(); ASSERT_TRUE(segment != nullptr); s = segment->add_column( std::make_shared("add_int32", DataType::INT32, false), "", AddColumnOptions()); EXPECT_FALSE(s.ok()); s = segment->add_column(std::make_shared( "add_undefined", DataType::UNDEFINED, false), "", AddColumnOptions()); EXPECT_FALSE(s.ok()); // before add column auto meta = segment->meta(); auto &persist_blocks = meta->persisted_blocks(); int old_scalar_blocks_cnt = 0; for (auto &block : persist_blocks) { if (block.type() == BlockType::SCALAR) { old_scalar_blocks_cnt++; } } int add_column_cnt = 0; auto func = [&](const std::shared_ptr &field_schema, const std::string &expression) { auto &column_name = field_schema->name(); AddColumnOptions add_options; status = segment->add_column(field_schema, expression, add_options); EXPECT_TRUE(status.ok()); // after add column int new_scalar_blocks_cnt = 0; for (auto &block : persist_blocks) { if (block.type() == BlockType::SCALAR) { new_scalar_blocks_cnt++; } } EXPECT_EQ( new_scalar_blocks_cnt, old_scalar_blocks_cnt + old_scalar_blocks_cnt * (++add_column_cnt)); auto combined_reader = segment->scan({"id", "name", "age", column_name}); ASSERT_TRUE(combined_reader != nullptr); std::shared_ptr batch; uint32_t total_doc = 0; while (true) { auto status = combined_reader->ReadNext(&batch); if (status.ok() == false) break; if (batch == nullptr) break; EXPECT_EQ(batch->num_columns(), 4); total_doc += batch->num_rows(); } EXPECT_EQ(total_doc, doc_count); auto new_schema = *schema_; new_schema.add_field(field_schema); auto check_doc = [&](int doc_count) { for (int i = 0; i < doc_count; i++) { auto expect_doc = test::TestHelper::CreateDoc(i, new_schema); auto doc = segment->Fetch(i); ASSERT_EQ(doc->pk(), expect_doc.pk()); // column in same persist block { ExecBatchPtr exec_batch = segment->fetch({"id", "name", "age"}, i); ASSERT_TRUE(exec_batch != nullptr); EXPECT_EQ(exec_batch->length, 1); EXPECT_EQ(exec_batch->values.size(), 3); auto id_scalar = exec_batch->values[0].scalar(); ASSERT_TRUE(id_scalar != nullptr); auto id_value = std::dynamic_pointer_cast(id_scalar); ASSERT_TRUE(id_value != nullptr); EXPECT_EQ(id_value->value, i); } { ExecBatchPtr exec_batch = segment->fetch({column_name}, i); ASSERT_TRUE(exec_batch != nullptr); EXPECT_EQ(exec_batch->length, 1); EXPECT_EQ(exec_batch->values.size(), 1); auto id_scalar = exec_batch->values[0].scalar(); ASSERT_TRUE(id_scalar != nullptr); } // column in different persist block { ExecBatchPtr exec_batch = segment->fetch({"id", "name", "age", column_name}, i); ASSERT_TRUE(exec_batch != nullptr); EXPECT_EQ(exec_batch->length, 1); EXPECT_EQ(exec_batch->values.size(), 4); auto id_scalar = exec_batch->values[0].scalar(); ASSERT_TRUE(id_scalar != nullptr); auto id_value = std::dynamic_pointer_cast(id_scalar); ASSERT_TRUE(id_value != nullptr); EXPECT_EQ(id_value->value, i); } } }; check_doc(doc_count); }; auto index_param = std::make_shared(); std::vector>> test_column_schemas = { {"add_int32", std::make_shared("", DataType::INT32, false, index_param)}, {"add_int64", std::make_shared("", DataType::INT64, false, index_param)}, {"add_uint32", std::make_shared("", DataType::UINT32, false, index_param)}, {"add_uint64", std::make_shared("", DataType::UINT64, false, index_param)}, {"add_float", std::make_shared("", DataType::FLOAT, false, index_param)}, {"add_double", std::make_shared("", DataType::DOUBLE, false, index_param)}, {"add_int32_nullable", std::make_shared( "", DataType::INT32, true, index_param)}, {"add_int64_nullable", std::make_shared( "", DataType::INT64, true, index_param)}, {"add_uint32_nullable", std::make_shared( "", DataType::UINT32, true, index_param)}, {"add_uint64_nullable", std::make_shared( "", DataType::UINT64, true, index_param)}, {"add_float_nullable", std::make_shared( "", DataType::FLOAT, true, index_param)}, {"add_double_nullable", std::make_shared( "", DataType::DOUBLE, true, index_param)}, }; std::unordered_map> test_expressions = { {"add_int32", {"int32 + 1", "-int32", "+int32", "1", "-1"}}, {"add_int64", {"int64 + 1", "-int64", "+int64", "1", "-1"}}, {"add_uint32", {"uint32 + 1", "-uint32", "+int32", "1", "0"}}, {"add_uint64", {"uint64 + 1", "-uint64", "+uint64", "1", "0"}}, {"add_float", {"float + 1.0", "-float", "+float", "0.1", "-0.1"}}, {"add_double", {"double + 1.0", "-double", "+double", "0.1", "-0.1"}}, {"add_int32_nullable", {""}}, {"add_int64_nullable", {""}}, {"add_uint32_nullable", {""}}, {"add_uint64_nullable", {""}}, {"add_float_nullable", {""}}, {"add_double_nullable", {""}}, }; for (auto &[column_name, field_schema] : test_column_schemas) { auto expressions = test_expressions[column_name]; for (auto &expression : expressions) { std::string generated_col_name = column_name + "_" + std::to_string( ailego::Crc32c::Hash(expression.data(), expression.size())); auto new_field_schema = std::make_shared( field_schema->name(), field_schema->data_type(), field_schema->nullable(), field_schema->index_params()); new_field_schema->set_name(generated_col_name); func(new_field_schema, expression); } } } TEST_P(SegmentTest, AddNullableColumnWithoutExpressionMultiBlock) { // Use small buffer to force multiple scalar blocks within a single segment options_.max_buffer_size_ = 1 * 1024; int doc_count = 100; auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, doc_count); ASSERT_TRUE(segment != nullptr); segment->dump(); auto writing_segment_meta = segment->meta(); Version version = version_manager_->get_current_version(); writing_segment_meta->remove_writing_forward_block(); auto s = version.add_persisted_segment_meta(writing_segment_meta); ASSERT_TRUE(s.ok()); s = version_manager_->apply(version); ASSERT_TRUE(s.ok()); s = version_manager_->flush(); ASSERT_TRUE(s.ok()); segment.reset(); version_manager_.reset(); id_map_->flush(); id_map_.reset(); std::string delete_store_path = FileHelper::MakeFilePath(col_path_, FileID::DELETE_FILE, 0); delete_store_->flush(delete_store_path); delete_store_.reset(); auto recover_version_manager = VersionManager::Recovery(col_path_); auto recover_version_mgr = recover_version_manager.value(); ASSERT_TRUE(recover_version_mgr != nullptr); Version v = recover_version_mgr->get_current_version(); const auto &persist_metas = v.persisted_segment_metas(); std::string idmap_path = FileHelper::MakeFilePath(col_path_, FileID::ID_FILE, v.id_map_path_suffix()); IDMap::Ptr recover_id_map = std::make_shared(col_name_); auto status = recover_id_map->open(idmap_path, false, false); ASSERT_TRUE(status.ok()); delete_store_path = FileHelper::MakeFilePath(col_path_, FileID::DELETE_FILE, v.delete_snapshot_path_suffix()); auto recover_delete_store = DeleteStore::CreateAndLoad(col_name_, delete_store_path); ASSERT_TRUE(recover_delete_store != nullptr); // Verify we have multiple scalar blocks int scalar_block_count = 0; for (auto &block : persist_metas[0]->persisted_blocks()) { if (block.type() == BlockType::SCALAR) { scalar_block_count++; } } ASSERT_GT(scalar_block_count, 1); options_.read_only_ = true; auto result = Segment::Open(col_path_, *schema_, *persist_metas[0], recover_id_map, recover_delete_store, recover_version_mgr, options_); ASSERT_TRUE(result.has_value()); segment = std::move(result).value(); ASSERT_TRUE(segment != nullptr); // Add nullable columns without expression — this used to crash on // multi-block segments std::vector> nullable_types = { {"add_int32_null", DataType::INT32}, {"add_uint32_null", DataType::UINT32}, {"add_int64_null", DataType::INT64}, }; for (auto &[nullable_col_name, data_type] : nullable_types) { auto field_schema = std::make_shared(nullable_col_name, data_type, true); s = segment->add_column(field_schema, "", AddColumnOptions()); ASSERT_TRUE(s.ok()) << "Failed to add nullable column " << nullable_col_name << ": " << s.message(); auto combined_reader = segment->scan({"id", "name", "age", nullable_col_name}); ASSERT_TRUE(combined_reader != nullptr); std::shared_ptr batch; uint32_t total_doc = 0; while (true) { auto st = combined_reader->ReadNext(&batch); if (!st.ok()) break; if (batch == nullptr) break; EXPECT_EQ(batch->num_columns(), 4); total_doc += batch->num_rows(); } EXPECT_EQ(total_doc, doc_count); } } TEST_P(SegmentTest, AddColumnWithExpressionMultiBlock) { // Use small buffer to force multiple scalar blocks within a single segment options_.max_buffer_size_ = 1 * 1024; int doc_count = 100; auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, doc_count); ASSERT_TRUE(segment != nullptr); segment->dump(); auto writing_segment_meta = segment->meta(); Version version = version_manager_->get_current_version(); writing_segment_meta->remove_writing_forward_block(); auto s = version.add_persisted_segment_meta(writing_segment_meta); ASSERT_TRUE(s.ok()); s = version_manager_->apply(version); ASSERT_TRUE(s.ok()); s = version_manager_->flush(); ASSERT_TRUE(s.ok()); segment.reset(); version_manager_.reset(); id_map_->flush(); id_map_.reset(); std::string delete_store_path = FileHelper::MakeFilePath(col_path_, FileID::DELETE_FILE, 0); delete_store_->flush(delete_store_path); delete_store_.reset(); auto recover_version_manager = VersionManager::Recovery(col_path_); auto recover_version_mgr = recover_version_manager.value(); ASSERT_TRUE(recover_version_mgr != nullptr); Version v = recover_version_mgr->get_current_version(); const auto &persist_metas = v.persisted_segment_metas(); std::string idmap_path = FileHelper::MakeFilePath(col_path_, FileID::ID_FILE, v.id_map_path_suffix()); IDMap::Ptr recover_id_map = std::make_shared(col_name_); auto status = recover_id_map->open(idmap_path, false, false); ASSERT_TRUE(status.ok()); delete_store_path = FileHelper::MakeFilePath(col_path_, FileID::DELETE_FILE, v.delete_snapshot_path_suffix()); auto recover_delete_store = DeleteStore::CreateAndLoad(col_name_, delete_store_path); ASSERT_TRUE(recover_delete_store != nullptr); // Verify we have multiple scalar blocks int scalar_block_count = 0; for (auto &block : persist_metas[0]->persisted_blocks()) { if (block.type() == BlockType::SCALAR) { scalar_block_count++; } } ASSERT_GT(scalar_block_count, 1); options_.read_only_ = true; auto result = Segment::Open(col_path_, *schema_, *persist_metas[0], recover_id_map, recover_delete_store, recover_version_mgr, options_); ASSERT_TRUE(result.has_value()); segment = std::move(result).value(); ASSERT_TRUE(segment != nullptr); // Add column with expression on multi-block segment auto field_schema = std::make_shared("add_expr_col", DataType::INT32, false); s = segment->add_column(field_schema, "id + 1", AddColumnOptions()); ASSERT_TRUE(s.ok()) << "Failed to add column with expression: " << s.message(); auto combined_reader = segment->scan({"id", "name", "age", "add_expr_col"}); ASSERT_TRUE(combined_reader != nullptr); std::shared_ptr batch; uint32_t total_doc = 0; while (true) { auto st = combined_reader->ReadNext(&batch); if (!st.ok()) break; if (batch == nullptr) break; EXPECT_EQ(batch->num_columns(), 4); total_doc += batch->num_rows(); } EXPECT_EQ(total_doc, doc_count); // Add nullable column without expression on the same multi-block segment auto nullable_field = std::make_shared("add_null_col", DataType::INT64, true); s = segment->add_column(nullable_field, "", AddColumnOptions()); ASSERT_TRUE(s.ok()) << "Failed to add nullable column after expression column: " << s.message(); combined_reader = segment->scan({"id", "add_expr_col", "add_null_col"}); ASSERT_TRUE(combined_reader != nullptr); total_doc = 0; while (true) { auto st = combined_reader->ReadNext(&batch); if (!st.ok()) break; if (batch == nullptr) break; EXPECT_EQ(batch->num_columns(), 3); total_doc += batch->num_rows(); } EXPECT_EQ(total_doc, doc_count); } TEST_P(SegmentTest, AlterColumnMultiBlock) { options_.max_buffer_size_ = 1 * 1024; int doc_count = 100; auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, doc_count); ASSERT_TRUE(segment != nullptr); segment->dump(); auto writing_segment_meta = segment->meta(); Version version = version_manager_->get_current_version(); writing_segment_meta->remove_writing_forward_block(); auto s = version.add_persisted_segment_meta(writing_segment_meta); ASSERT_TRUE(s.ok()); s = version_manager_->apply(version); ASSERT_TRUE(s.ok()); s = version_manager_->flush(); ASSERT_TRUE(s.ok()); segment.reset(); version_manager_.reset(); id_map_->flush(); id_map_.reset(); std::string delete_store_path = FileHelper::MakeFilePath(col_path_, FileID::DELETE_FILE, 0); delete_store_->flush(delete_store_path); delete_store_.reset(); auto recover_version_manager = VersionManager::Recovery(col_path_); auto recover_version_mgr = recover_version_manager.value(); ASSERT_TRUE(recover_version_mgr != nullptr); Version v = recover_version_mgr->get_current_version(); const auto &persist_metas = v.persisted_segment_metas(); std::string idmap_path = FileHelper::MakeFilePath(col_path_, FileID::ID_FILE, v.id_map_path_suffix()); IDMap::Ptr recover_id_map = std::make_shared(col_name_); auto status = recover_id_map->open(idmap_path, false, false); ASSERT_TRUE(status.ok()); delete_store_path = FileHelper::MakeFilePath(col_path_, FileID::DELETE_FILE, v.delete_snapshot_path_suffix()); auto recover_delete_store = DeleteStore::CreateAndLoad(col_name_, delete_store_path); ASSERT_TRUE(recover_delete_store != nullptr); int scalar_block_count = 0; for (auto &block : persist_metas[0]->persisted_blocks()) { if (block.type() == BlockType::SCALAR) { scalar_block_count++; } } ASSERT_GT(scalar_block_count, 1); options_.read_only_ = true; auto result = Segment::Open(col_path_, *schema_, *persist_metas[0], recover_id_map, recover_delete_store, recover_version_mgr, options_); ASSERT_TRUE(result.has_value()); segment = std::move(result).value(); ASSERT_TRUE(segment != nullptr); // Alter column type: int32 -> int64 on multi-block segment auto new_field = std::make_shared("id", DataType::INT64, false); s = segment->alter_column("id", new_field, AlterColumnOptions()); ASSERT_TRUE(s.ok()) << "alter_column failed: " << s.message(); auto combined_reader = segment->scan({"id", "name", "age"}); ASSERT_TRUE(combined_reader != nullptr); std::shared_ptr batch; uint32_t total_doc = 0; while (true) { auto st = combined_reader->ReadNext(&batch); if (!st.ok()) break; if (batch == nullptr) break; EXPECT_EQ(batch->num_columns(), 3); total_doc += batch->num_rows(); } EXPECT_EQ(total_doc, doc_count); } TEST_P(SegmentTest, DropColumnMultiBlock) { options_.max_buffer_size_ = 1 * 1024; int doc_count = 100; auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, doc_count); ASSERT_TRUE(segment != nullptr); segment->dump(); auto writing_segment_meta = segment->meta(); Version version = version_manager_->get_current_version(); writing_segment_meta->remove_writing_forward_block(); auto s = version.add_persisted_segment_meta(writing_segment_meta); ASSERT_TRUE(s.ok()); s = version_manager_->apply(version); ASSERT_TRUE(s.ok()); s = version_manager_->flush(); ASSERT_TRUE(s.ok()); segment.reset(); version_manager_.reset(); id_map_->flush(); id_map_.reset(); std::string delete_store_path = FileHelper::MakeFilePath(col_path_, FileID::DELETE_FILE, 0); delete_store_->flush(delete_store_path); delete_store_.reset(); auto recover_version_manager = VersionManager::Recovery(col_path_); auto recover_version_mgr = recover_version_manager.value(); ASSERT_TRUE(recover_version_mgr != nullptr); Version v = recover_version_mgr->get_current_version(); const auto &persist_metas = v.persisted_segment_metas(); std::string idmap_path = FileHelper::MakeFilePath(col_path_, FileID::ID_FILE, v.id_map_path_suffix()); IDMap::Ptr recover_id_map = std::make_shared(col_name_); auto status = recover_id_map->open(idmap_path, false, false); ASSERT_TRUE(status.ok()); delete_store_path = FileHelper::MakeFilePath(col_path_, FileID::DELETE_FILE, v.delete_snapshot_path_suffix()); auto recover_delete_store = DeleteStore::CreateAndLoad(col_name_, delete_store_path); ASSERT_TRUE(recover_delete_store != nullptr); int scalar_block_count = 0; for (auto &block : persist_metas[0]->persisted_blocks()) { if (block.type() == BlockType::SCALAR) { scalar_block_count++; } } ASSERT_GT(scalar_block_count, 1); options_.read_only_ = true; auto result = Segment::Open(col_path_, *schema_, *persist_metas[0], recover_id_map, recover_delete_store, recover_version_mgr, options_); ASSERT_TRUE(result.has_value()); segment = std::move(result).value(); ASSERT_TRUE(segment != nullptr); // Drop column on multi-block segment s = segment->drop_column("id"); ASSERT_TRUE(s.ok()) << "drop_column failed: " << s.message(); auto combined_reader = segment->scan({"id"}); ASSERT_TRUE(combined_reader == nullptr); // Remaining columns should still be scannable combined_reader = segment->scan({"name", "age"}); ASSERT_TRUE(combined_reader != nullptr); std::shared_ptr batch; uint32_t total_doc = 0; while (true) { auto st = combined_reader->ReadNext(&batch); if (!st.ok()) break; if (batch == nullptr) break; EXPECT_EQ(batch->num_columns(), 2); total_doc += batch->num_rows(); } EXPECT_EQ(total_doc, doc_count); } TEST_P(SegmentTest, AddNullableThenAlterDropMultiBlock) { options_.max_buffer_size_ = 1 * 1024; int doc_count = 100; auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, doc_count); ASSERT_TRUE(segment != nullptr); segment->dump(); auto writing_segment_meta = segment->meta(); Version version = version_manager_->get_current_version(); writing_segment_meta->remove_writing_forward_block(); auto s = version.add_persisted_segment_meta(writing_segment_meta); ASSERT_TRUE(s.ok()); s = version_manager_->apply(version); ASSERT_TRUE(s.ok()); s = version_manager_->flush(); ASSERT_TRUE(s.ok()); segment.reset(); version_manager_.reset(); id_map_->flush(); id_map_.reset(); std::string delete_store_path = FileHelper::MakeFilePath(col_path_, FileID::DELETE_FILE, 0); delete_store_->flush(delete_store_path); delete_store_.reset(); auto recover_version_manager = VersionManager::Recovery(col_path_); auto recover_version_mgr = recover_version_manager.value(); ASSERT_TRUE(recover_version_mgr != nullptr); Version v = recover_version_mgr->get_current_version(); const auto &persist_metas = v.persisted_segment_metas(); std::string idmap_path = FileHelper::MakeFilePath(col_path_, FileID::ID_FILE, v.id_map_path_suffix()); IDMap::Ptr recover_id_map = std::make_shared(col_name_); auto status = recover_id_map->open(idmap_path, false, false); ASSERT_TRUE(status.ok()); delete_store_path = FileHelper::MakeFilePath(col_path_, FileID::DELETE_FILE, v.delete_snapshot_path_suffix()); auto recover_delete_store = DeleteStore::CreateAndLoad(col_name_, delete_store_path); ASSERT_TRUE(recover_delete_store != nullptr); options_.read_only_ = true; auto result = Segment::Open(col_path_, *schema_, *persist_metas[0], recover_id_map, recover_delete_store, recover_version_mgr, options_); ASSERT_TRUE(result.has_value()); segment = std::move(result).value(); ASSERT_TRUE(segment != nullptr); // 1) Add nullable column without expression auto nullable_field = std::make_shared("nullable_col", DataType::INT32, true); s = segment->add_column(nullable_field, "", AddColumnOptions()); ASSERT_TRUE(s.ok()) << "add nullable column failed: " << s.message(); // 2) Alter the nullable column type: INT32 -> INT64 auto altered_field = std::make_shared("nullable_col", DataType::INT64, true); s = segment->alter_column("nullable_col", altered_field, AlterColumnOptions()); ASSERT_TRUE(s.ok()) << "alter nullable column failed: " << s.message(); auto combined_reader = segment->scan({"id", "nullable_col"}); ASSERT_TRUE(combined_reader != nullptr); std::shared_ptr batch; uint32_t total_doc = 0; while (true) { auto st = combined_reader->ReadNext(&batch); if (!st.ok()) break; if (batch == nullptr) break; EXPECT_EQ(batch->num_columns(), 2); total_doc += batch->num_rows(); } EXPECT_EQ(total_doc, doc_count); // 3) Drop the altered nullable column s = segment->drop_column("nullable_col"); ASSERT_TRUE(s.ok()) << "drop nullable column failed: " << s.message(); combined_reader = segment->scan({"nullable_col"}); ASSERT_TRUE(combined_reader == nullptr); // 4) Add it back again to verify structure is still consistent auto re_add_field = std::make_shared("nullable_col_v2", DataType::FLOAT, true); s = segment->add_column(re_add_field, "", AddColumnOptions()); ASSERT_TRUE(s.ok()) << "re-add nullable column failed: " << s.message(); combined_reader = segment->scan({"id", "name", "nullable_col_v2"}); ASSERT_TRUE(combined_reader != nullptr); total_doc = 0; while (true) { auto st = combined_reader->ReadNext(&batch); if (!st.ok()) break; if (batch == nullptr) break; EXPECT_EQ(batch->num_columns(), 3); total_doc += batch->num_rows(); } EXPECT_EQ(total_doc, doc_count); } TEST_P(SegmentTest, AlterColumn) { // create segment int doc_count = 1000; auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, doc_count); ASSERT_TRUE(segment != nullptr); auto s = segment->alter_column( "alter_int32", std::make_shared("alter_int32", DataType::INT32, false), AlterColumnOptions()); EXPECT_FALSE(s.ok()); segment->dump(); auto writing_segment_meta = segment->meta(); // convert writing segment meta to persisted segment meta Version version = version_manager_->get_current_version(); writing_segment_meta->remove_writing_forward_block(); s = version.add_persisted_segment_meta(writing_segment_meta); ASSERT_TRUE(s.ok()); s = version_manager_->apply(version); ASSERT_TRUE(s.ok()); s = version_manager_->flush(); ASSERT_TRUE(s.ok()); segment.reset(); version_manager_.reset(); id_map_->flush(); id_map_.reset(); std::string delete_store_path = FileHelper::MakeFilePath(col_path_, FileID::DELETE_FILE, 0); delete_store_->flush(delete_store_path); delete_store_.reset(); auto recover_version_manager = VersionManager::Recovery(col_path_); auto recover_version_mgr = recover_version_manager.value(); ASSERT_TRUE(recover_version_mgr != nullptr); Version v = recover_version_mgr->get_current_version(); const auto &persist_metas = v.persisted_segment_metas(); // idmap std::string idmap_path = FileHelper::MakeFilePath(col_path_, FileID::ID_FILE, v.id_map_path_suffix()); IDMap::Ptr recover_id_map = std::make_shared(col_name_); auto status = recover_id_map->open(idmap_path, false, false); ASSERT_TRUE(status.ok()); delete_store_path = FileHelper::MakeFilePath(col_path_, FileID::DELETE_FILE, v.delete_snapshot_path_suffix()); auto recover_delete_store = DeleteStore::CreateAndLoad(col_name_, delete_store_path); ASSERT_TRUE(recover_delete_store != nullptr); // open persist segment options_.read_only_ = true; auto result = Segment::Open(col_path_, *schema_, *persist_metas[0], recover_id_map, recover_delete_store, recover_version_mgr, options_); ASSERT_TRUE(result.has_value()); segment = std::move(result).value(); ASSERT_TRUE(segment != nullptr); s = segment->alter_column( "alter_int32", std::make_shared("alter_int32", DataType::INT32, false), AlterColumnOptions()); EXPECT_FALSE(s.ok()); // not found s = segment->alter_column( "int32", std::make_shared("int32", DataType::UNDEFINED, false), AlterColumnOptions()); EXPECT_FALSE(s.ok()); // undefined type auto func = [&](const std::string &column_name, const std::shared_ptr &field_schema) { AlterColumnOptions alter_options; status = segment->alter_column(column_name, field_schema, alter_options); EXPECT_TRUE(status.ok()); auto combined_reader = segment->scan({"id", "name", "age", column_name}); ASSERT_TRUE(combined_reader != nullptr); std::shared_ptr batch; uint32_t total_doc = 0; while (true) { auto status = combined_reader->ReadNext(&batch); if (status.ok() == false) break; if (batch == nullptr) break; EXPECT_EQ(batch->num_columns(), 4); total_doc += batch->num_rows(); } EXPECT_EQ(total_doc, doc_count); }; std::vector test_alter_columns = {"int32", "int64", "uint32", "uint64", "float", "double"}; for (auto &column_name : test_alter_columns) { // std::string column_name = "int32"; for (auto &dest_column : test_alter_columns) { if (column_name == dest_column) continue; auto field_schema = schema_->get_field(dest_column); auto new_field_schema = std::make_shared(*field_schema); new_field_schema->set_name(column_name); func(column_name, new_field_schema); } } } TEST_P(SegmentTest, DropColumn) { // create segment int doc_count = 1000; auto segment = test::TestHelper::CreateSegmentWithDoc( col_path_, *schema_, 0, 0, id_map_, delete_store_, version_manager_, options_, 0, doc_count); ASSERT_TRUE(segment != nullptr); auto s = segment->drop_column("int32"); EXPECT_FALSE(s.ok()); segment->dump(); auto writing_segment_meta = segment->meta(); // convert writing segment meta to persisted segment meta Version version = version_manager_->get_current_version(); writing_segment_meta->remove_writing_forward_block(); s = version.add_persisted_segment_meta(writing_segment_meta); ASSERT_TRUE(s.ok()); s = version_manager_->apply(version); ASSERT_TRUE(s.ok()); s = version_manager_->flush(); ASSERT_TRUE(s.ok()); segment.reset(); version_manager_.reset(); id_map_->flush(); id_map_.reset(); std::string delete_store_path = FileHelper::MakeFilePath(col_path_, FileID::DELETE_FILE, 0); delete_store_->flush(delete_store_path); delete_store_.reset(); auto recover_version_manager = VersionManager::Recovery(col_path_); auto recover_version_mgr = recover_version_manager.value(); ASSERT_TRUE(recover_version_mgr != nullptr); Version v = recover_version_mgr->get_current_version(); const auto &persist_metas = v.persisted_segment_metas(); // idmap std::string idmap_path = FileHelper::MakeFilePath(col_path_, FileID::ID_FILE, v.id_map_path_suffix()); IDMap::Ptr recover_id_map = std::make_shared(col_name_); auto status = recover_id_map->open(idmap_path, false, false); ASSERT_TRUE(status.ok()); delete_store_path = FileHelper::MakeFilePath(col_path_, FileID::DELETE_FILE, v.delete_snapshot_path_suffix()); auto recover_delete_store = DeleteStore::CreateAndLoad(col_name_, delete_store_path); ASSERT_TRUE(recover_delete_store != nullptr); // open persist segment options_.read_only_ = true; auto result = Segment::Open(col_path_, *schema_, *persist_metas[0], recover_id_map, recover_delete_store, recover_version_mgr, options_); ASSERT_TRUE(result.has_value()); segment = std::move(result).value(); ASSERT_TRUE(segment != nullptr); auto meta = segment->meta(); auto &persist_blocks = meta->persisted_blocks(); auto func = [&](const std::string &column_name) { status = segment->drop_column(column_name); EXPECT_TRUE(status.ok()); // after drop column bool col_exit = false; for (auto &block : persist_blocks) { if (block.type() == BlockType::SCALAR) { if (block.contain_column(column_name)) { col_exit = true; break; } } } EXPECT_EQ(col_exit, false); auto combined_reader = segment->scan({column_name}); ASSERT_TRUE(combined_reader == nullptr); }; std::vector test_drop_columns = {"int32", "int64", "uint32", "uint64", "float", "double"}; for (auto &column_name : test_drop_columns) { func(column_name); } } INSTANTIATE_TEST_SUITE_P(MMapTest, SegmentTest, testing::Values(true, false));