blob: 2564fec8f1cab7c1225a092f5dea9586657b31aa [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. 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.
#include <gtest/gtest.h>
#include <algorithm>
#include <map>
#include "core/data_type/data_type_string.h"
#include "storage/predicate/predicate_creator.h"
#include "testutil/index_storage_test_util.h"
namespace doris::index_storage_test {
namespace {
std::shared_ptr<ColumnPredicate> title_equals(std::string value) {
return create_comparison_predicate<PredicateType::EQ>(
1, "title", std::make_shared<DataTypeString>(),
Field::create_field<TYPE_STRING>(std::move(value)), false);
}
void expect_applied_title_index(const IndexReadResult& result, int64_t expected_filtered_rows) {
expect_index_filter_stats(result, expected_filtered_rows);
}
} // namespace
class IndexStorageLifecycleTest : public IndexStorageTestFixture {};
TEST_F(IndexStorageLifecycleTest, TextIndexHitAfterCumulativeCompaction) {
const auto index_case =
IndexStorageCaseBuilder("text_index_hit_after_cumulative_compaction")
.tablet_id(110001)
.text_column(TextColumnSpec {.unique_id = 2, .name = "title"})
.inverted_index(IndexSpec::column_index(20001, "idx_title", 2))
.rowset(0, IndexDataSourceSpec::inline_text({"hello", "other"}, 0))
.rowset(1, IndexDataSourceSpec::inline_text({"hello", "other"}, 100))
.build();
ASSERT_TRUE(create_tablet(index_case.tablet_options).ok());
auto rowsets = write_rowsets(index_case.rowsets);
ASSERT_TRUE(rowsets.has_value()) << rowsets.error();
IndexReadOptions read_options;
read_options.predicates.push_back(title_equals("hello"));
auto read_result = read_rowsets(rowsets.value(), read_options);
ASSERT_TRUE(read_result.has_value()) << read_result.error();
EXPECT_EQ(read_result->rows_read, 2);
expect_applied_title_index(read_result.value(), 2);
auto compacted = compact_rowsets(IndexCompactionKind::CUMULATIVE, rowsets.value());
ASSERT_TRUE(compacted.has_value()) << compacted.error();
ASSERT_NE(compacted.value(), nullptr);
EXPECT_EQ(compacted.value()->num_rows(), 4);
auto reloaded = reload_rowsets({compacted.value()});
ASSERT_TRUE(reloaded.has_value()) << reloaded.error();
IndexReadOptions compacted_read_options;
compacted_read_options.predicates.push_back(title_equals("hello"));
auto compacted_read = read_rowsets(reloaded.value(), compacted_read_options);
ASSERT_TRUE(compacted_read.has_value()) << compacted_read.error();
EXPECT_EQ(compacted_read->rows_read, 2);
expect_applied_title_index(compacted_read.value(), 2);
}
TEST_F(IndexStorageLifecycleTest, TextIndexHitAfterFullCompaction) {
IndexTabletOptions options;
options.tablet_id = 110007;
options.text_columns = {TextColumnSpec {.unique_id = 2, .name = "title"}};
options.inverted_indexes.push_back(IndexSpec::column_index(20001, "idx_title", 2));
ASSERT_TRUE(create_tablet(options).ok());
IndexRowsetSpec rowset0;
rowset0.version = 0;
rowset0.batches.push_back(IndexBatch::single_text({"hello", "other"}, 0));
auto rowset0_result = write_rowset(rowset0);
ASSERT_TRUE(rowset0_result.has_value()) << rowset0_result.error();
IndexRowsetSpec rowset1;
rowset1.version = 1;
rowset1.batches.push_back(IndexBatch::single_text({"other", "hello"}, 100));
auto rowset1_result = write_rowset(rowset1);
ASSERT_TRUE(rowset1_result.has_value()) << rowset1_result.error();
auto compacted = compact_rowsets(IndexCompactionKind::FULL,
{rowset0_result.value(), rowset1_result.value()});
ASSERT_TRUE(compacted.has_value()) << compacted.error();
ASSERT_NE(compacted.value(), nullptr);
EXPECT_EQ(compacted.value()->num_rows(), 4);
IndexReadOptions read_options;
read_options.predicates.push_back(title_equals("hello"));
auto read_result = read_rowsets({compacted.value()}, read_options);
ASSERT_TRUE(read_result.has_value()) << read_result.error();
EXPECT_EQ(read_result->rows_read, 2);
expect_applied_title_index(read_result.value(), 2);
}
TEST_F(IndexStorageLifecycleTest, CountOnIndexPredicateFiltersRows) {
IndexTabletOptions options;
options.tablet_id = 110008;
options.text_columns = {TextColumnSpec {.unique_id = 2, .name = "title"}};
options.inverted_indexes.push_back(IndexSpec::column_index(20001, "idx_title", 2));
ASSERT_TRUE(create_tablet(options).ok());
IndexRowsetSpec rowset;
rowset.version = 0;
rowset.batches.push_back(IndexBatch::single_text({"hello", "other", "hello"}, 0));
auto rowset_result = write_rowset(rowset);
ASSERT_TRUE(rowset_result.has_value()) << rowset_result.error();
IndexReadOptions read_options;
read_options.push_down_agg_type_opt = TPushAggOp::COUNT_ON_INDEX;
read_options.predicates.push_back(title_equals("hello"));
auto read_result = read_rowsets({rowset_result.value()}, read_options);
ASSERT_TRUE(read_result.has_value()) << read_result.error();
EXPECT_EQ(read_result->rows_read, 2);
expect_applied_title_index(read_result.value(), 1);
}
TEST_F(IndexStorageLifecycleTest, DISABLED_CountOnIndexSkipsReadingKeyDataWhenIndexApplied) {
IndexTabletOptions options;
options.tablet_id = 110027;
options.text_columns = {TextColumnSpec {.unique_id = 2, .name = "title"}};
options.inverted_indexes.push_back(IndexSpec::column_index(20001, "idx_title", 2));
ASSERT_TRUE(create_tablet(options).ok());
IndexRowsetSpec rowset;
rowset.version = 0;
rowset.batches.push_back(IndexBatch::single_text({"hello", "other", "hello"}, 0));
auto rowset_result = write_rowset(rowset);
ASSERT_TRUE(rowset_result.has_value()) << rowset_result.error();
ScopedDebugPoint fail_if_key_column_is_read("segment_iterator._read_columns_by_index",
{{"column_name", "k"}});
IndexReadOptions read_options;
read_options.push_down_agg_type_opt = TPushAggOp::COUNT_ON_INDEX;
read_options.predicates.push_back(title_equals("hello"));
auto read_result = read_rowsets({rowset_result.value()}, read_options);
ASSERT_TRUE(read_result.has_value()) << read_result.error();
EXPECT_EQ(read_result->rows_read, 2);
EXPECT_EQ(read_result->stats.raw_rows_read, 2);
expect_applied_title_index(read_result.value(), 1);
}
TEST_F(IndexStorageLifecycleTest, TextPredicateFiltersRowsAfterNullBitmapCheck) {
IndexTabletOptions options;
options.tablet_id = 110010;
options.text_columns = {TextColumnSpec {.unique_id = 2, .name = "title"}};
options.inverted_indexes.push_back(
IndexSpec::column_index(20001, "idx_title_fulltext", 2, {{"parser", "english"}}));
options.inverted_indexes.push_back(IndexSpec::column_index(20002, "idx_title_string", 2));
ASSERT_TRUE(create_tablet(options).ok());
IndexRowsetSpec rowset;
rowset.version = 0;
rowset.data_sources.push_back(
IndexDataSourceSpec::inline_text({"hello", "hello world", "other"}, 0));
auto rowset_result = write_rowset(rowset);
ASSERT_TRUE(rowset_result.has_value()) << rowset_result.error();
IndexReadOptions read_options;
read_options.predicates.push_back(title_equals("hello"));
auto read_result = read_rowsets({rowset_result.value()}, read_options);
ASSERT_TRUE(read_result.has_value()) << read_result.error();
EXPECT_EQ(read_result->rows_read, 1);
expect_applied_title_index(read_result.value(), 2);
}
TEST_F(IndexStorageLifecycleTest, V1IndexStorageWritesOneFilePerIndex) {
IndexTabletOptions options;
options.tablet_id = 110019;
options.index_storage_format = InvertedIndexStorageFormatPB::V1;
options.text_columns = {TextColumnSpec {.unique_id = 2, .name = "title"}};
options.inverted_indexes.push_back(
IndexSpec::column_index(20001, "idx_title_fulltext", 2, {{"parser", "english"}}));
options.inverted_indexes.push_back(IndexSpec::column_index(20002, "idx_title_string", 2));
ASSERT_TRUE(create_tablet(options).ok());
IndexRowsetSpec rowset;
rowset.version = 0;
rowset.batches.push_back(IndexBatch::single_text({"hello", "hello world", "other"}, 0));
auto rowset_result = write_rowset(rowset);
ASSERT_TRUE(rowset_result.has_value()) << rowset_result.error();
auto probe = probe_rowset(rowset_result.value());
ASSERT_TRUE(probe.has_value()) << probe.error();
EXPECT_EQ(probe->num_segments, 1);
EXPECT_EQ(probe->index_files.expected_files, 2);
expect_index_files(probe.value(), true);
IndexReadOptions read_options;
read_options.predicates.push_back(title_equals("hello"));
auto read_result = read_rowsets({rowset_result.value()}, read_options);
ASSERT_TRUE(read_result.has_value()) << read_result.error();
EXPECT_EQ(read_result->rows_read, 1);
expect_applied_title_index(read_result.value(), 2);
}
TEST_F(IndexStorageLifecycleTest, V2IndexStorageWritesCompoundIndexFile) {
IndexTabletOptions options;
options.tablet_id = 110020;
options.index_storage_format = InvertedIndexStorageFormatPB::V2;
options.text_columns = {TextColumnSpec {.unique_id = 2, .name = "title"}};
options.inverted_indexes.push_back(
IndexSpec::column_index(20001, "idx_title_fulltext", 2, {{"parser", "english"}}));
options.inverted_indexes.push_back(IndexSpec::column_index(20002, "idx_title_string", 2));
ASSERT_TRUE(create_tablet(options).ok());
IndexRowsetSpec rowset;
rowset.version = 0;
rowset.batches.push_back(IndexBatch::single_text({"hello", "hello world", "other"}, 0));
auto rowset_result = write_rowset(rowset);
ASSERT_TRUE(rowset_result.has_value()) << rowset_result.error();
auto probe = probe_rowset(rowset_result.value());
ASSERT_TRUE(probe.has_value()) << probe.error();
EXPECT_EQ(probe->num_segments, 1);
EXPECT_EQ(probe->index_files.expected_files, 1);
EXPECT_GT(probe->index_files.rowset_meta_entries, 0);
expect_index_files(probe.value(), true);
IndexReadOptions read_options;
read_options.predicates.push_back(title_equals("hello"));
auto read_result = read_rowsets({rowset_result.value()}, read_options);
ASSERT_TRUE(read_result.has_value()) << read_result.error();
EXPECT_EQ(read_result->rows_read, 1);
expect_applied_title_index(read_result.value(), 2);
}
TEST_F(IndexStorageLifecycleTest, TextIndexDisabledKeepsFilterStatsAtZero) {
IndexTabletOptions options;
options.tablet_id = 110013;
options.text_columns = {TextColumnSpec {.unique_id = 2, .name = "title"}};
options.inverted_indexes.push_back(IndexSpec::column_index(20001, "idx_title", 2));
ASSERT_TRUE(create_tablet(options).ok());
IndexRowsetSpec rowset;
rowset.version = 0;
rowset.batches.push_back(IndexBatch::single_text({"hello", "other", "hello"}, 0));
auto rowset_result = write_rowset(rowset);
ASSERT_TRUE(rowset_result.has_value()) << rowset_result.error();
IndexReadOptions read_options;
read_options.enable_inverted_index_query = false;
read_options.predicates.push_back(title_equals("hello"));
auto read_result = read_rowsets({rowset_result.value()}, read_options);
ASSERT_TRUE(read_result.has_value()) << read_result.error();
EXPECT_EQ(read_result->rows_read, 2);
EXPECT_EQ(read_result->stats.rows_inverted_index_filtered, 0);
expect_index_filter_stats(read_result.value(), 0);
}
TEST_F(IndexStorageLifecycleTest, TextPredicateWithoutIndexKeepsFilterStatsAtZero) {
IndexTabletOptions options;
options.tablet_id = 110004;
options.text_columns = {TextColumnSpec {.unique_id = 2, .name = "title"}};
ASSERT_TRUE(create_tablet(options).ok());
IndexRowsetSpec rowset;
rowset.version = 0;
rowset.batches.push_back(IndexBatch::single_text({"hello", "other"}, 0));
auto rowset_result = write_rowset(rowset);
ASSERT_TRUE(rowset_result.has_value()) << rowset_result.error();
IndexReadOptions read_options;
read_options.predicates.push_back(title_equals("hello"));
auto read_result = read_rowsets({rowset_result.value()}, read_options);
ASSERT_TRUE(read_result.has_value()) << read_result.error();
EXPECT_EQ(read_result->rows_read, 1);
EXPECT_EQ(read_result->stats.rows_inverted_index_filtered, 0);
expect_index_filter_stats(read_result.value(), 0);
}
TEST_F(IndexStorageLifecycleTest, DropTextIndexKeepsFilterStatsAtZero) {
const auto index_case =
IndexStorageCaseBuilder("drop_text_index_keeps_filter_stats_at_zero")
.tablet_id(110005)
.text_column(TextColumnSpec {.unique_id = 2, .name = "title"})
.inverted_index(IndexSpec::column_index(20001, "idx_title", 2))
.rowset(0, IndexDataSourceSpec::inline_text({"hello", "other"}, 0))
.build();
ASSERT_TRUE(create_tablet(index_case.tablet_options).ok());
auto rowsets = write_rowsets(index_case.rowsets);
ASSERT_TRUE(rowsets.has_value()) << rowsets.error();
IndexReadOptions before_drop_options;
before_drop_options.predicates.push_back(title_equals("hello"));
auto before_drop = read_rowsets(rowsets.value(), before_drop_options);
ASSERT_TRUE(before_drop.has_value()) << before_drop.error();
EXPECT_EQ(before_drop->rows_read, 1);
expect_applied_title_index(before_drop.value(), 1);
auto dropped_rowsets = drop_inverted_indexes({IndexSpec::column_index(20001, "idx_title", 2)});
ASSERT_TRUE(dropped_rowsets.has_value()) << dropped_rowsets.error();
ASSERT_EQ(dropped_rowsets->size(), 1);
auto dropped_probe = probe_rowset(dropped_rowsets->front());
ASSERT_TRUE(dropped_probe.has_value()) << dropped_probe.error();
expect_index_files(dropped_probe.value(), false);
auto reloaded_dropped_rowsets = reload_rowsets(dropped_rowsets.value());
ASSERT_TRUE(reloaded_dropped_rowsets.has_value()) << reloaded_dropped_rowsets.error();
IndexReadOptions after_drop_options;
after_drop_options.predicates.push_back(title_equals("hello"));
auto after_drop = read_rowsets(reloaded_dropped_rowsets.value(), after_drop_options);
ASSERT_TRUE(after_drop.has_value()) << after_drop.error();
EXPECT_EQ(after_drop->rows_read, 1);
EXPECT_EQ(after_drop->stats.rows_inverted_index_filtered, 0);
expect_index_filter_stats(after_drop.value(), 0);
}
TEST_F(IndexStorageLifecycleTest, BuildTextIndexAfterExistingRowsUsesNewIndex) {
const auto index_case =
IndexStorageCaseBuilder("build_text_index_after_existing_rows_uses_new_index")
.tablet_id(110011)
.text_column(TextColumnSpec {.unique_id = 2, .name = "title"})
.rowset(0, IndexDataSourceSpec::inline_text({"hello", "other"}, 0))
.rowset(1, IndexDataSourceSpec::inline_text({"other", "hello"}, 100))
.build();
ASSERT_TRUE(create_tablet(index_case.tablet_options).ok());
auto rowsets = write_rowsets(index_case.rowsets);
ASSERT_TRUE(rowsets.has_value()) << rowsets.error();
IndexReadOptions before_build_options;
before_build_options.predicates.push_back(title_equals("hello"));
auto before_build = read_rowsets(rowsets.value(), before_build_options);
ASSERT_TRUE(before_build.has_value()) << before_build.error();
EXPECT_EQ(before_build->rows_read, 2);
EXPECT_EQ(before_build->stats.rows_inverted_index_filtered, 0);
expect_index_filter_stats(before_build.value(), 0);
const auto built_index = IndexSpec::column_index(20003, "idx_title_built", 2);
auto built_rowsets = build_inverted_indexes({built_index});
ASSERT_TRUE(built_rowsets.has_value()) << built_rowsets.error();
ASSERT_EQ(built_rowsets->size(), 2);
for (const auto& rowset : built_rowsets.value()) {
auto probe = probe_rowset(rowset);
ASSERT_TRUE(probe.has_value()) << probe.error();
expect_index_files(probe.value(), true);
}
auto reloaded_built_rowsets = reload_rowsets(built_rowsets.value());
ASSERT_TRUE(reloaded_built_rowsets.has_value()) << reloaded_built_rowsets.error();
IndexReadOptions after_build_options;
after_build_options.predicates.push_back(title_equals("hello"));
auto after_build = read_rowsets(reloaded_built_rowsets.value(), after_build_options);
ASSERT_TRUE(after_build.has_value()) << after_build.error();
EXPECT_EQ(after_build->rows_read, 2);
expect_applied_title_index(after_build.value(), 2);
}
TEST_F(IndexStorageLifecycleTest, PatchedSchemaKeepsExistingTextIndexUsable) {
IndexTabletOptions options;
options.tablet_id = 110015;
options.text_columns = {TextColumnSpec {.unique_id = 2, .name = "title"}};
options.inverted_indexes.push_back(IndexSpec::column_index(20001, "idx_title", 2));
ASSERT_TRUE(create_tablet(options).ok());
IndexRowsetSpec rowset;
rowset.version = 0;
rowset.batches.push_back(IndexBatch::single_text({"hello", "other"}, 0));
auto rowset_result = write_rowset(rowset);
ASSERT_TRUE(rowset_result.has_value()) << rowset_result.error();
IndexSchemaPatch patch;
patch.add_text_columns.push_back(
TextColumnSpec {.unique_id = 3, .name = "body", .nullable = true});
auto patched_schema = build_patched_tablet_schema(*tablet_schema(), patch);
ASSERT_NE(patched_schema, nullptr);
ASSERT_TRUE(patched_schema->has_column_unique_id(3));
auto patched_rowsets =
inject_reader_schema_for_rowsets({rowset_result.value()}, std::move(patched_schema));
ASSERT_TRUE(patched_rowsets.has_value()) << patched_rowsets.error();
IndexReadOptions read_options;
read_options.return_columns = {0, 1};
read_options.predicates.push_back(title_equals("hello"));
auto read_result = read_rowsets(patched_rowsets.value(), read_options);
ASSERT_TRUE(read_result.has_value()) << read_result.error();
EXPECT_EQ(read_result->rows_read, 1);
expect_applied_title_index(read_result.value(), 1);
}
TEST_F(IndexStorageLifecycleTest, InjectReaderSchemaUpdatesTabletRowsetMapForIndexDrop) {
IndexTabletOptions options;
options.tablet_id = 110019;
options.text_columns = {TextColumnSpec {.unique_id = 2, .name = "title"}};
ASSERT_TRUE(create_tablet(options).ok());
IndexRowsetSpec rowset;
rowset.version = 0;
rowset.batches.push_back(IndexBatch::single_text({"hello", "other"}, 0));
auto rowset_result = write_rowset(rowset);
ASSERT_TRUE(rowset_result.has_value()) << rowset_result.error();
auto index = IndexSpec::column_index(20008, "idx_title_patch_drop", 2);
IndexSchemaPatch patch;
patch.add_inverted_indexes.push_back(index);
auto patched_schema = build_patched_tablet_schema(*tablet_schema(), patch);
ASSERT_NE(patched_schema, nullptr);
ASSERT_TRUE(patched_schema->has_inverted_index_with_index_id(20008));
auto patched_rowsets =
inject_reader_schema_for_rowsets({rowset_result.value()}, std::move(patched_schema));
ASSERT_TRUE(patched_rowsets.has_value()) << patched_rowsets.error();
ASSERT_EQ(patched_rowsets->size(), 1);
ASSERT_TRUE(patched_rowsets->front()->tablet_schema()->has_inverted_index_with_index_id(20008));
auto dropped_rowsets = drop_inverted_indexes({index});
ASSERT_TRUE(dropped_rowsets.has_value()) << dropped_rowsets.error();
ASSERT_EQ(dropped_rowsets->size(), 1);
EXPECT_FALSE(
dropped_rowsets->front()->tablet_schema()->has_inverted_index_with_index_id(20008));
}
TEST_F(IndexStorageLifecycleTest, PatchedSchemaAddDropTextColumnReadsDefaultsAndCompacts) {
IndexTabletOptions options;
options.tablet_id = 110016;
options.text_columns = {
TextColumnSpec {.unique_id = 2, .name = "title"},
TextColumnSpec {.unique_id = 3, .name = "obsolete", .nullable = true},
};
options.inverted_indexes.push_back(IndexSpec::column_index(20001, "idx_title", 2));
options.inverted_indexes.push_back(IndexSpec::column_index(20002, "idx_obsolete", 3));
ASSERT_TRUE(create_tablet(options).ok());
IndexRowsetSpec rowset0;
rowset0.version = 0;
IndexBatch batch0;
batch0.keys = {0, 1};
batch0.text_values_by_column = {{"hello", "other"}, {"drop-a", "drop-b"}};
rowset0.batches.push_back(std::move(batch0));
auto rowset0_result = write_rowset(rowset0);
ASSERT_TRUE(rowset0_result.has_value()) << rowset0_result.error();
IndexRowsetSpec rowset1;
rowset1.version = 1;
IndexBatch batch1;
batch1.keys = {2, 3};
batch1.text_values_by_column = {{"hello", "later"}, {"drop-c", "drop-d"}};
rowset1.batches.push_back(std::move(batch1));
auto rowset1_result = write_rowset(rowset1);
ASSERT_TRUE(rowset1_result.has_value()) << rowset1_result.error();
IndexSchemaPatch patch;
patch.drop_column_uids.insert(3);
patch.add_text_columns.push_back(
TextColumnSpec {.unique_id = 4, .name = "body", .nullable = true});
auto patched_schema = build_patched_tablet_schema(*tablet_schema(), patch);
ASSERT_NE(patched_schema, nullptr);
EXPECT_FALSE(patched_schema->has_column_unique_id(3));
ASSERT_TRUE(patched_schema->has_column_unique_id(4));
auto patched_rowsets = inject_reader_schema_for_rowsets(
{rowset0_result.value(), rowset1_result.value()}, std::move(patched_schema));
ASSERT_TRUE(patched_rowsets.has_value()) << patched_rowsets.error();
IndexReadOptions read_options;
read_options.collect_string_values = true;
read_options.need_ordered_result = true;
read_options.return_columns = {1, 2};
auto read_result = read_rowsets(patched_rowsets.value(), read_options);
ASSERT_TRUE(read_result.has_value()) << read_result.error();
EXPECT_EQ(read_result->rows_read, 4);
ASSERT_TRUE(read_result->string_values_by_uid.contains(2));
ASSERT_TRUE(read_result->string_values_by_uid.contains(4));
EXPECT_EQ(read_result->string_values_by_uid.at(2),
(std::vector<std::optional<std::string>> {"hello", "other", "hello", "later"}));
EXPECT_EQ(read_result->string_values_by_uid.at(4),
(std::vector<std::optional<std::string>> {std::nullopt, std::nullopt, std::nullopt,
std::nullopt}));
IndexReadOptions index_read_options;
index_read_options.predicates.push_back(title_equals("hello"));
auto index_read = read_rowsets(patched_rowsets.value(), index_read_options);
ASSERT_TRUE(index_read.has_value()) << index_read.error();
EXPECT_EQ(index_read->rows_read, 2);
expect_applied_title_index(index_read.value(), 2);
auto compacted = compact_rowsets(IndexCompactionKind::CUMULATIVE, patched_rowsets.value());
ASSERT_TRUE(compacted.has_value()) << compacted.error();
ASSERT_NE(compacted.value(), nullptr);
EXPECT_EQ(compacted.value()->num_rows(), 4);
IndexReadOptions compacted_read_options;
compacted_read_options.collect_string_values = true;
compacted_read_options.need_ordered_result = true;
compacted_read_options.return_columns = {1, 2};
auto compacted_read = read_rowsets({compacted.value()}, compacted_read_options);
ASSERT_TRUE(compacted_read.has_value()) << compacted_read.error();
EXPECT_EQ(compacted_read->rows_read, 4);
ASSERT_TRUE(compacted_read->string_values_by_uid.contains(2));
ASSERT_TRUE(compacted_read->string_values_by_uid.contains(4));
EXPECT_EQ(compacted_read->string_values_by_uid.at(2),
(std::vector<std::optional<std::string>> {"hello", "other", "hello", "later"}));
EXPECT_EQ(compacted_read->string_values_by_uid.at(4),
(std::vector<std::optional<std::string>> {std::nullopt, std::nullopt, std::nullopt,
std::nullopt}));
}
} // namespace doris::index_storage_test