| // 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 <crc32c/crc32c.h> |
| #include <gen_cpp/olap_file.pb.h> |
| #include <gen_cpp/segment_v2.pb.h> |
| #include <gflags/gflags.h> |
| |
| #include <cctype> |
| #include <charconv> |
| #include <cstring> |
| #include <filesystem> |
| #include <fstream> |
| #include <iomanip> |
| #include <iostream> |
| #include <limits> |
| #include <set> |
| #include <sstream> |
| #include <string> |
| #include <string_view> |
| #include <vector> |
| |
| #include "common/logging.h" |
| #include "common/status.h" |
| #include "core/assert_cast.h" |
| #include "core/column/column.h" |
| #include "core/column/column_nullable.h" |
| #include "core/column/column_string.h" |
| #include "core/column/column_vector.h" |
| #include "core/data_type/data_type_factory.hpp" |
| #include "core/decimal12.h" |
| #include "core/field.h" |
| #include "core/types.h" |
| #include "core/value/large_int_value.h" |
| #include "cpp/private_member_accessor.hpp" |
| #include "io/fs/file_reader.h" |
| #include "io/fs/local_file_system.h" |
| #include "json2pb/pb_to_json.h" |
| #include "runtime/exec_env.h" |
| #include "runtime/memory/cache_manager.h" |
| #include "runtime/memory/mem_tracker_limiter.h" |
| #include "storage/data_dir.h" |
| #include "storage/olap_common.h" |
| #include "storage/options.h" |
| #include "storage/segment/column_reader.h" |
| #include "storage/segment/common.h" |
| #include "storage/segment/encoding_info.h" |
| #include "storage/segment/page_pointer.h" |
| #include "storage/storage_engine.h" |
| #include "storage/tablet/tablet_column_object_pool.h" |
| #include "storage/tablet/tablet_meta.h" |
| #include "storage/tablet/tablet_meta_manager.h" |
| #include "storage/tablet/tablet_schema.h" |
| #include "storage/tablet/tablet_schema_cache.h" |
| #include "storage/types.h" |
| #include "util/coding.h" |
| #include "util/unaligned.h" |
| |
| using doris::DataDir; |
| using doris::StorageEngine; |
| using doris::Status; |
| using doris::TabletMeta; |
| using doris::TabletMetaManager; |
| using doris::Slice; |
| using doris::segment_v2::SegmentFooterPB; |
| using doris::io::FileReaderSPtr; |
| |
| using namespace doris::segment_v2; |
| using namespace doris; |
| using namespace doris; |
| |
| DEFINE_string(root_path, "", "storage root path"); |
| DEFINE_string(operation, "get_meta", |
| "valid operation: get_meta, flag, load_meta, delete_meta, show_meta, " |
| "show_segment_footer, show_segment_data, check_page_crc, scan_page_crc, " |
| "gen_empty_segment"); |
| DEFINE_int64(tablet_id, 0, "tablet_id for tablet meta"); |
| DEFINE_int32(num_rows_per_block, 1024, "num rows per block"); |
| DEFINE_int32(schema_hash, 0, "schema_hash for tablet meta"); |
| DEFINE_string(json_meta_path, "", "absolute json meta file path"); |
| DEFINE_string(pb_meta_path, "", "pb meta file path"); |
| DEFINE_string(tablet_file, "", "file to save a set of tablets"); |
| DEFINE_string(file, "", "segment file path"); |
| DEFINE_string(output_path, "", "output directory path (default: current directory)"); |
| DEFINE_int32(num_short_key_columns, 0, "number of short key columns"); |
| DEFINE_bool(has_sequence_col, false, "whether has sequence column"); |
| DEFINE_bool(enable_unique_key_merge_on_write, false, "whether enable unique key merge on write"); |
| DEFINE_bool(scan_segment_pages, false, |
| "scan every data page referenced by the ordinal index and report checksum ranges"); |
| DEFINE_int64(rows, 10, |
| "maximum logical rows to read per column for show_segment_data; -1 means all rows"); |
| DEFINE_uint64(row_start, 0, "first logical row ordinal to read for show_segment_data"); |
| DEFINE_uint64(batch_rows, 4096, "maximum rows per read batch for show_segment_data"); |
| DEFINE_bool(check_only, false, "decode rows without printing values for show_segment_data"); |
| DEFINE_bool(verify_checksum, true, |
| "verify page checksums while decoding data for show_segment_data"); |
| DEFINE_string(page_ranges, "", |
| "comma-separated OFFSET:SIZE ranges for the check_page_crc operation"); |
| DEFINE_string(page_ranges_file, "", |
| "file containing OFFSET SIZE pairs for the check_page_crc operation"); |
| DEFINE_uint64(page_scan_start, 0, "known first page offset for the scan_page_crc operation"); |
| DEFINE_uint64(page_scan_end, 0, "exclusive scan end offset for the scan_page_crc operation"); |
| DEFINE_string(page_output, "all", "page CRC output level: all, errors, or summary"); |
| |
| std::string get_usage(const std::string& progname) { |
| std::stringstream ss; |
| ss << progname << " is the Doris BE Meta tool.\n"; |
| ss << "Stop BE first before use this tool.\n"; |
| ss << "Usage:\n"; |
| ss << "./meta_tool --operation=get_meta --root_path=/path/to/storage/path " |
| "--tablet_id=tabletid --schema_hash=schemahash\n"; |
| ss << "./meta_tool --operation=load_meta --root_path=/path/to/storage/path " |
| "--json_meta_path=path\n"; |
| ss << "./meta_tool --operation=delete_meta " |
| "--root_path=/path/to/storage/path --tablet_id=tabletid " |
| "--schema_hash=schemahash\n"; |
| ss << "./meta_tool --operation=batch_delete_meta --tablet_file=file_path\n"; |
| ss << "./meta_tool --operation=show_meta --pb_meta_path=path\n"; |
| ss << "./meta_tool --operation=show_segment_footer --file=/path/to/segment/file\n"; |
| ss << "./meta_tool --operation=show_segment_data --file=/path/to/segment/file " |
| "[--row_start=N] [--rows=N|-1] [--batch_rows=N] [--check_only] " |
| "[--verify_checksum]\n"; |
| ss << "./meta_tool --operation=check_page_crc --file=/path/to/segment/file " |
| "(--page_ranges=OFFSET:SIZE,... | --page_ranges_file=/path/to/ranges) " |
| "[--page_output=all|errors|summary]\n"; |
| ss << "./meta_tool --operation=scan_page_crc --file=/path/to/segment/file " |
| "--page_scan_start=OFFSET --page_scan_end=OFFSET " |
| "[--page_output=all|errors|summary]\n"; |
| ss << "./meta_tool --operation=gen_empty_segment [--output_path=/path/to/output]\n"; |
| ss << " Generates an empty segment file (0 rows) at specified path or current directory\n"; |
| ss << " Default output file name: empty.dat\n"; |
| return ss.str(); |
| } |
| |
| void show_meta() { |
| TabletMeta tablet_meta; |
| Status s = tablet_meta.create_from_file(FLAGS_pb_meta_path); |
| if (!s.ok()) { |
| std::cout << "load pb meta file:" << FLAGS_pb_meta_path << " failed" |
| << ", status:" << s << std::endl; |
| return; |
| } |
| std::string json_meta; |
| json2pb::Pb2JsonOptions json_options; |
| json_options.pretty_json = true; |
| doris::TabletMetaPB tablet_meta_pb; |
| tablet_meta.to_meta_pb(&tablet_meta_pb, false); |
| json2pb::ProtoMessageToJson(tablet_meta_pb, &json_meta, json_options); |
| std::cout << json_meta << std::endl; |
| } |
| |
| void get_meta(DataDir* data_dir) { |
| std::string value; |
| Status s = |
| TabletMetaManager::get_json_meta(data_dir, FLAGS_tablet_id, FLAGS_schema_hash, &value); |
| if (!s.ok()) { |
| if (s.is<doris::ErrorCode::META_KEY_NOT_FOUND>()) { |
| std::cout << "no tablet meta for tablet_id:" << FLAGS_tablet_id |
| << ", schema_hash:" << FLAGS_schema_hash << std::endl; |
| } else { |
| std::cout << "get meta failed: " << s.to_string() << std::endl; |
| } |
| return; |
| } |
| std::cout << value << std::endl; |
| } |
| |
| void load_meta(DataDir* data_dir) { |
| // load json tablet meta into meta |
| Status s = TabletMetaManager::load_json_meta(data_dir, FLAGS_json_meta_path); |
| if (!s.ok()) { |
| std::cout << "load meta failed, status:" << s << std::endl; |
| return; |
| } |
| std::cout << "load meta successfully" << std::endl; |
| } |
| |
| void delete_meta(DataDir* data_dir) { |
| Status s = TabletMetaManager::remove(data_dir, FLAGS_tablet_id, FLAGS_schema_hash); |
| if (!s.ok()) { |
| std::cout << "delete tablet meta failed for tablet_id:" << FLAGS_tablet_id |
| << ", schema_hash:" << FLAGS_schema_hash << ", status:" << s << std::endl; |
| return; |
| } |
| std::cout << "delete meta successfully" << std::endl; |
| } |
| |
| Status init_data_dir(StorageEngine& engine, const std::string& dir, std::unique_ptr<DataDir>* ret) { |
| std::string root_path; |
| RETURN_IF_ERROR(doris::io::global_local_filesystem()->canonicalize(dir, &root_path)); |
| doris::StorePath path; |
| auto res = parse_root_path(root_path, &path); |
| if (!res.ok()) { |
| std::cout << "parse root path failed:" << root_path << std::endl; |
| return Status::InternalError("parse root path failed"); |
| } |
| |
| auto p = std::make_unique<DataDir>(engine, path.path, path.capacity_bytes, path.storage_medium); |
| if (p == nullptr) { |
| std::cout << "new data dir failed" << std::endl; |
| return Status::InternalError("new data dir failed"); |
| } |
| res = p->init(); |
| if (!res.ok()) { |
| std::cout << "data_dir load failed: " << res.to_string() << std::endl; |
| return res; |
| } |
| |
| p.swap(*ret); |
| return Status::OK(); |
| } |
| |
| void batch_delete_meta(const std::string& tablet_file) { |
| // each line in tablet file indicate a tablet to delete, format is: |
| // data_dir,tablet_id,schema_hash |
| // eg: |
| // /data1/palo.HDD,100010,11212389324 |
| // /data2/palo.HDD,100010,23049230234 |
| std::ifstream infile(tablet_file); |
| std::string line = ""; |
| int err_num = 0; |
| int delete_num = 0; |
| int total_num = 0; |
| StorageEngine engine(doris::EngineOptions {}); |
| std::unordered_map<std::string, std::unique_ptr<DataDir>> dir_map; |
| while (std::getline(infile, line)) { |
| total_num++; |
| std::vector<std::string> v = absl::StrSplit(line, ","); |
| if (v.size() != 3) { |
| std::cout << "invalid line in tablet_file: " << line << std::endl; |
| err_num++; |
| continue; |
| } |
| // 1. get dir |
| std::string dir; |
| Status st = doris::io::global_local_filesystem()->canonicalize(v[0], &dir); |
| if (!st.ok()) { |
| std::cout << "invalid root dir in tablet_file: " << line << std::endl; |
| err_num++; |
| continue; |
| } |
| |
| if (dir_map.find(dir) == dir_map.end()) { |
| // new data dir, init it |
| std::unique_ptr<DataDir> data_dir_p; |
| st = init_data_dir(engine, dir, &data_dir_p); |
| if (!st.ok()) { |
| std::cout << "invalid root path:" << FLAGS_root_path |
| << ", error: " << st.to_string() << std::endl; |
| err_num++; |
| continue; |
| } |
| dir_map[dir] = std::move(data_dir_p); |
| std::cout << "get a new data dir: " << dir << std::endl; |
| } |
| DataDir* data_dir = dir_map[dir].get(); |
| if (data_dir == nullptr) { |
| std::cout << "failed to get data dir: " << line << std::endl; |
| err_num++; |
| continue; |
| } |
| |
| // 2. get tablet id/schema_hash |
| int64_t tablet_id; |
| if (!absl::SimpleAtoi(v[1], &tablet_id)) { |
| std::cout << "invalid tablet id: " << line << std::endl; |
| err_num++; |
| continue; |
| } |
| int64_t schema_hash; |
| if (!absl::SimpleAtoi(v[2], &schema_hash)) { |
| std::cout << "invalid schema hash: " << line << std::endl; |
| err_num++; |
| continue; |
| } |
| |
| Status s = TabletMetaManager::remove(data_dir, tablet_id, schema_hash); |
| if (!s.ok()) { |
| std::cout << "delete tablet meta failed for tablet_id:" << tablet_id |
| << ", schema_hash:" << schema_hash << ", status:" << s << std::endl; |
| err_num++; |
| continue; |
| } |
| |
| delete_num++; |
| } |
| |
| std::cout << "total: " << total_num << ", delete: " << delete_num << ", error: " << err_num |
| << std::endl; |
| return; |
| } |
| |
| Status get_segment_footer(doris::io::FileReader* file_reader, SegmentFooterPB* footer) { |
| // Footer := SegmentFooterPB, FooterPBSize(4), FooterPBChecksum(4), MagicNumber(4) |
| std::string file_name = file_reader->path(); |
| uint64_t file_size = file_reader->size(); |
| if (file_size < 12) { |
| return Status::Corruption("Bad segment file {}: file size {} < 12", file_name, file_size); |
| } |
| |
| size_t bytes_read = 0; |
| uint8_t fixed_buf[12]; |
| Slice slice(fixed_buf, 12); |
| RETURN_IF_ERROR(file_reader->read_at(file_size - 12, slice, &bytes_read)); |
| |
| // validate magic number |
| if (memcmp(fixed_buf + 8, doris::segment_v2::k_segment_magic, |
| doris::segment_v2::k_segment_magic_length) != 0) { |
| return Status::Corruption("Bad segment file {}: magic number not match", file_name); |
| } |
| |
| // read footer PB |
| uint32_t footer_length = doris::decode_fixed32_le(fixed_buf); |
| if (file_size < 12 + footer_length) { |
| return Status::Corruption("Bad segment file {}: file size {} < {}", file_name, file_size, |
| 12 + footer_length); |
| } |
| std::string footer_buf; |
| footer_buf.resize(footer_length); |
| Slice slice2(footer_buf); |
| RETURN_IF_ERROR(file_reader->read_at(file_size - 12 - footer_length, slice2, &bytes_read)); |
| |
| // validate footer PB's checksum |
| uint32_t expect_checksum = doris::decode_fixed32_le(fixed_buf + 4); |
| uint32_t actual_checksum = crc32c::Crc32c(footer_buf.data(), footer_buf.size()); |
| if (actual_checksum != expect_checksum) { |
| return Status::Corruption( |
| "Bad segment file {}: footer checksum not match, actual={} vs expect={}", file_name, |
| actual_checksum, expect_checksum); |
| } |
| |
| // deserialize footer PB |
| if (!footer->ParseFromString(footer_buf)) { |
| return Status::Corruption("Bad segment file {}: failed to parse SegmentFooterPB", |
| file_name); |
| } |
| return Status::OK(); |
| } |
| |
| void show_segment_footer(const std::string& file_name) { |
| doris::io::FileReaderSPtr file_reader; |
| Status status = doris::io::global_local_filesystem()->open_file(file_name, &file_reader); |
| if (!status.ok()) { |
| std::cout << "open file failed: " << status << std::endl; |
| return; |
| } |
| SegmentFooterPB footer; |
| status = get_segment_footer(file_reader.get(), &footer); |
| if (!status.ok()) { |
| std::cout << "get footer failed: " << status.to_string() << std::endl; |
| return; |
| } |
| std::string json_footer; |
| json2pb::Pb2JsonOptions json_options; |
| json_options.pretty_json = true; |
| bool ret = json2pb::ProtoMessageToJson(footer, &json_footer, json_options); |
| if (!ret) { |
| std::cout << "Convert PB to json failed" << std::endl; |
| return; |
| } |
| std::cout << json_footer << std::endl; |
| return; |
| } |
| |
| // Helper function to get field type string |
| std::string get_field_type_string(doris::FieldType type) { |
| switch (type) { |
| case doris::FieldType::OLAP_FIELD_TYPE_TINYINT: |
| return "TINYINT"; |
| case doris::FieldType::OLAP_FIELD_TYPE_SMALLINT: |
| return "SMALLINT"; |
| case doris::FieldType::OLAP_FIELD_TYPE_INT: |
| return "INT"; |
| case doris::FieldType::OLAP_FIELD_TYPE_BIGINT: |
| return "BIGINT"; |
| case doris::FieldType::OLAP_FIELD_TYPE_LARGEINT: |
| return "LARGEINT"; |
| case doris::FieldType::OLAP_FIELD_TYPE_FLOAT: |
| return "FLOAT"; |
| case doris::FieldType::OLAP_FIELD_TYPE_DOUBLE: |
| return "DOUBLE"; |
| case doris::FieldType::OLAP_FIELD_TYPE_DECIMAL: |
| return "DECIMAL"; |
| case doris::FieldType::OLAP_FIELD_TYPE_DECIMAL32: |
| return "DECIMAL32"; |
| case doris::FieldType::OLAP_FIELD_TYPE_DECIMAL64: |
| return "DECIMAL64"; |
| case doris::FieldType::OLAP_FIELD_TYPE_DECIMAL128I: |
| return "DECIMAL128I"; |
| case doris::FieldType::OLAP_FIELD_TYPE_CHAR: |
| return "CHAR"; |
| case doris::FieldType::OLAP_FIELD_TYPE_VARCHAR: |
| return "VARCHAR"; |
| case doris::FieldType::OLAP_FIELD_TYPE_STRING: |
| return "STRING"; |
| case doris::FieldType::OLAP_FIELD_TYPE_DATE: |
| return "DATE"; |
| case doris::FieldType::OLAP_FIELD_TYPE_DATETIME: |
| return "DATETIME"; |
| case doris::FieldType::OLAP_FIELD_TYPE_DATEV2: |
| return "DATEV2"; |
| case doris::FieldType::OLAP_FIELD_TYPE_DATETIMEV2: |
| return "DATETIMEV2"; |
| case doris::FieldType::OLAP_FIELD_TYPE_BOOL: |
| return "BOOLEAN"; |
| case doris::FieldType::OLAP_FIELD_TYPE_STRUCT: |
| return "STRUCT"; |
| case doris::FieldType::OLAP_FIELD_TYPE_ARRAY: |
| return "ARRAY"; |
| case doris::FieldType::OLAP_FIELD_TYPE_MAP: |
| return "MAP"; |
| case doris::FieldType::OLAP_FIELD_TYPE_JSONB: |
| return "JSONB"; |
| case doris::FieldType::OLAP_FIELD_TYPE_HLL: |
| return "HLL"; |
| case doris::FieldType::OLAP_FIELD_TYPE_BITMAP: |
| return "BITMAP"; |
| case doris::FieldType::OLAP_FIELD_TYPE_QUANTILE_STATE: |
| return "QUANTILE_STATE"; |
| case doris::FieldType::OLAP_FIELD_TYPE_AGG_STATE: |
| return "AGG_STATE"; |
| case doris::FieldType::OLAP_FIELD_TYPE_VARIANT: |
| return "VARIANT"; |
| default: |
| return "UNKNOWN"; |
| } |
| } |
| |
| // Helper function to get encoding type string |
| std::string get_encoding_string(doris::segment_v2::EncodingTypePB encoding) { |
| switch (encoding) { |
| case doris::segment_v2::PLAIN_ENCODING: |
| return "PLAIN"; |
| case doris::segment_v2::PREFIX_ENCODING: |
| return "PREFIX"; |
| case doris::segment_v2::RLE: |
| return "RLE"; |
| case doris::segment_v2::DICT_ENCODING: |
| return "DICT_ENCODING"; |
| case doris::segment_v2::BIT_SHUFFLE: |
| return "BIT_SHUFFLE"; |
| case doris::segment_v2::FOR_ENCODING: |
| return "FOR_ENCODING"; |
| case doris::segment_v2::PLAIN_ENCODING_V2: |
| return "PLAIN_ENCODING_V2"; |
| default: |
| return "UNKNOWN"; |
| } |
| } |
| |
| // Helper function to get compression type string |
| std::string get_compression_string(doris::segment_v2::CompressionTypePB compression) { |
| switch (compression) { |
| case doris::segment_v2::NO_COMPRESSION: |
| return "NONE"; |
| case doris::segment_v2::SNAPPY: |
| return "SNAPPY"; |
| case doris::segment_v2::LZ4: |
| return "LZ4"; |
| case doris::segment_v2::LZ4F: |
| return "LZ4F"; |
| case doris::segment_v2::ZLIB: |
| return "ZLIB"; |
| case doris::segment_v2::ZSTD: |
| return "ZSTD"; |
| case doris::segment_v2::LZ4HC: |
| return "LZ4HC"; |
| default: |
| return "UNKNOWN"; |
| } |
| } |
| |
| // Helper function to format a single value from a column |
| std::string format_column_value(const doris::IColumn& column, size_t row, |
| doris::FieldType field_type) { |
| try { |
| switch (field_type) { |
| case FieldType::OLAP_FIELD_TYPE_BOOL: { |
| return column.get_bool(row) ? "true" : "false"; |
| } |
| case FieldType::OLAP_FIELD_TYPE_TINYINT: |
| case FieldType::OLAP_FIELD_TYPE_SMALLINT: |
| case FieldType::OLAP_FIELD_TYPE_INT: |
| case FieldType::OLAP_FIELD_TYPE_BIGINT: { |
| return std::to_string(column.get_int(row)); |
| } |
| case FieldType::OLAP_FIELD_TYPE_LARGEINT: { |
| // LargeInt is stored as Int128 |
| const StringRef& data = column.get_data_at(row); |
| if (data.size == sizeof(__int128)) { |
| // data.data may not be 16-byte aligned; use unaligned_load to avoid UB. |
| __int128 val = unaligned_load<__int128>(data.data); |
| return doris::LargeIntValue::to_string(val); |
| } |
| return "<invalid largeint>"; |
| } |
| case FieldType::OLAP_FIELD_TYPE_FLOAT: { |
| const StringRef& data = column.get_data_at(row); |
| if (data.size == sizeof(float)) { |
| float val = *reinterpret_cast<const float*>(data.data); |
| return std::to_string(val); |
| } |
| return "<invalid float>"; |
| } |
| case FieldType::OLAP_FIELD_TYPE_DOUBLE: { |
| const StringRef& data = column.get_data_at(row); |
| if (data.size == sizeof(double)) { |
| double val = *reinterpret_cast<const double*>(data.data); |
| return std::to_string(val); |
| } |
| return "<invalid double>"; |
| } |
| case FieldType::OLAP_FIELD_TYPE_DATE: |
| case FieldType::OLAP_FIELD_TYPE_DATEV2: { |
| const StringRef& data = column.get_data_at(row); |
| if (data.size == sizeof(uint32_t)) { |
| uint32_t val = *reinterpret_cast<const uint32_t*>(data.data); |
| return std::to_string(val); |
| } |
| return "<invalid date>"; |
| } |
| case FieldType::OLAP_FIELD_TYPE_DATETIME: |
| case FieldType::OLAP_FIELD_TYPE_DATETIMEV2: { |
| const StringRef& data = column.get_data_at(row); |
| if (data.size == sizeof(uint64_t)) { |
| uint64_t val = *reinterpret_cast<const uint64_t*>(data.data); |
| return std::to_string(val); |
| } |
| return "<invalid datetime>"; |
| } |
| case FieldType::OLAP_FIELD_TYPE_CHAR: |
| case FieldType::OLAP_FIELD_TYPE_VARCHAR: |
| case FieldType::OLAP_FIELD_TYPE_STRING: |
| case FieldType::OLAP_FIELD_TYPE_HLL: |
| case FieldType::OLAP_FIELD_TYPE_BITMAP: |
| case FieldType::OLAP_FIELD_TYPE_JSONB: |
| case FieldType::OLAP_FIELD_TYPE_QUANTILE_STATE: { |
| const StringRef& str = column.get_data_at(row); |
| std::string result = "'"; |
| for (size_t i = 0; i < str.size && i < 50; ++i) { |
| // Escape quotes and special characters |
| char c = str.data[i]; |
| if (c == '\0') { |
| result += "\\0"; |
| } else if (c == '\n') { |
| result += "\\n"; |
| } else if (c == '\r') { |
| result += "\\r"; |
| } else if (c == '\t') { |
| result += "\\t"; |
| } else if (c == '\'') { |
| result += "\\'"; |
| } else if (c == '\\') { |
| result += "\\\\"; |
| } else if (static_cast<unsigned char>(c) < 32) { |
| // Other control characters |
| char buf[8]; |
| snprintf(buf, sizeof(buf), "\\x%02x", static_cast<unsigned char>(c)); |
| result += buf; |
| } else { |
| result += c; |
| } |
| } |
| if (str.size > 50) { |
| result += "..."; |
| } |
| result += "'"; |
| return result; |
| } |
| case FieldType::OLAP_FIELD_TYPE_DECIMAL: |
| case FieldType::OLAP_FIELD_TYPE_DECIMAL32: |
| case FieldType::OLAP_FIELD_TYPE_DECIMAL64: |
| case FieldType::OLAP_FIELD_TYPE_DECIMAL128I: { |
| const StringRef& data = column.get_data_at(row); |
| if (data.size == sizeof(__int128)) { |
| // data.data may not be 16-byte aligned; use unaligned_load to avoid UB. |
| __int128 val = unaligned_load<__int128>(data.data); |
| return doris::LargeIntValue::to_string(val); |
| } |
| return "<invalid decimal>"; |
| } |
| default: |
| return "<unsupported type>"; |
| } |
| } catch (const std::exception& e) { |
| return "<error: " + std::string(e.what()) + ">"; |
| } |
| } |
| |
| enum class PageOutputMode { ALL, ERRORS, SUMMARY }; |
| |
| struct RawPageRange { |
| uint64_t offset = 0; |
| uint64_t size = 0; |
| }; |
| |
| struct PageChecksumScanResult { |
| uint64_t offset = 0; |
| uint64_t size = 0; |
| bool range_valid = false; |
| bool readable = false; |
| bool checksum_ok = false; |
| bool footer_size_ok = false; |
| bool footer_ok = false; |
| uint32_t actual_checksum = 0; |
| uint32_t expected_checksum = 0; |
| uint32_t footer_size = 0; |
| PageTypePB page_type = UNKNOWN_PAGE_TYPE; |
| std::string error; |
| |
| bool ok() const { |
| return range_valid && readable && checksum_ok && footer_size_ok && footer_ok; |
| } |
| }; |
| |
| struct PageCrcSummary { |
| uint64_t pages_checked = 0; |
| uint64_t valid = 0; |
| uint64_t bad_crc = 0; |
| uint64_t bad_footer = 0; |
| uint64_t unreadable = 0; |
| uint64_t invalid_range = 0; |
| |
| void add(const PageChecksumScanResult& result) { |
| ++pages_checked; |
| if (result.ok()) { |
| ++valid; |
| return; |
| } |
| if (!result.range_valid) { |
| ++invalid_range; |
| } else if (!result.readable) { |
| ++unreadable; |
| } else { |
| if (!result.checksum_ok) { |
| ++bad_crc; |
| } |
| if (!result.footer_size_ok || !result.footer_ok) { |
| ++bad_footer; |
| } |
| } |
| } |
| |
| bool ok() const { return pages_checked > 0 && valid == pages_checked; } |
| }; |
| |
| Status read_file_exact(const FileReaderSPtr& file_reader, uint64_t offset, char* data, |
| size_t size) { |
| size_t bytes_read = 0; |
| Status status = |
| file_reader->read_at(static_cast<size_t>(offset), Slice(data, size), &bytes_read); |
| if (!status.ok()) { |
| status.prepend("failed to read file at offset " + std::to_string(offset) + ": "); |
| return status; |
| } |
| if (bytes_read != size) { |
| return Status::IOError("short read at offset {}: expected={}, actual={}", offset, size, |
| bytes_read); |
| } |
| return Status::OK(); |
| } |
| |
| PageChecksumScanResult check_page_checksum(const FileReaderSPtr& file_reader, uint64_t offset, |
| uint64_t size) { |
| PageChecksumScanResult result; |
| result.offset = offset; |
| result.size = size; |
| if (size < 8) { |
| result.error = "size_less_than_8"; |
| return result; |
| } |
| |
| const uint64_t file_size = file_reader->size(); |
| if (offset > file_size || size > file_size - offset) { |
| result.error = "out_of_range"; |
| return result; |
| } |
| result.range_valid = true; |
| |
| char trailer[8]; |
| if (!read_file_exact(file_reader, offset + size - sizeof(trailer), trailer, sizeof(trailer)) |
| .ok()) { |
| result.error = "read_failed"; |
| return result; |
| } |
| result.footer_size = doris::decode_fixed32_le(reinterpret_cast<const uint8_t*>(trailer)); |
| result.expected_checksum = |
| doris::decode_fixed32_le(reinterpret_cast<const uint8_t*>(trailer + 4)); |
| |
| constexpr size_t crc_chunk_size = 1024 * 1024; |
| size_t buffer_size = static_cast<size_t>(std::min<uint64_t>(size - 4, crc_chunk_size)); |
| std::vector<char> buffer(buffer_size); |
| uint64_t cursor = offset; |
| uint64_t remaining = size - 4; |
| uint32_t checksum = 0; |
| while (remaining > 0) { |
| size_t bytes_to_read = static_cast<size_t>(std::min<uint64_t>(remaining, buffer.size())); |
| if (!read_file_exact(file_reader, cursor, buffer.data(), bytes_to_read).ok()) { |
| result.error = "read_failed"; |
| return result; |
| } |
| checksum = crc32c::Extend(checksum, reinterpret_cast<const uint8_t*>(buffer.data()), |
| bytes_to_read); |
| cursor += bytes_to_read; |
| remaining -= bytes_to_read; |
| } |
| result.readable = true; |
| result.actual_checksum = checksum; |
| result.checksum_ok = result.actual_checksum == result.expected_checksum; |
| |
| result.footer_size_ok = result.footer_size > 0 && result.footer_size <= size - 8 && |
| result.footer_size <= std::numeric_limits<int>::max(); |
| if (result.footer_size_ok) { |
| std::vector<char> footer_buffer(result.footer_size); |
| uint64_t footer_offset = offset + size - 8 - result.footer_size; |
| Status footer_status = read_file_exact(file_reader, footer_offset, footer_buffer.data(), |
| footer_buffer.size()); |
| if (footer_status.ok()) { |
| PageFooterPB footer; |
| result.footer_ok = footer.ParseFromArray(footer_buffer.data(), |
| static_cast<int>(footer_buffer.size())) && |
| footer.has_type() && footer.has_uncompressed_size(); |
| if (result.footer_ok) { |
| result.page_type = footer.type(); |
| } |
| } |
| } |
| |
| if (!result.checksum_ok) { |
| result.error = "checksum_mismatch"; |
| } else if (!result.footer_size_ok) { |
| result.error = "invalid_footer_size"; |
| } else if (!result.footer_ok) { |
| result.error = "invalid_footer"; |
| } |
| return result; |
| } |
| |
| PageChecksumScanResult scan_page_checksum(const FileReaderSPtr& file_reader, |
| const PagePointer& page_pointer) { |
| return check_page_checksum(file_reader, page_pointer.offset, page_pointer.size); |
| } |
| |
| std::string_view trim_ascii(std::string_view value) { |
| while (!value.empty() && std::isspace(static_cast<unsigned char>(value.front()))) { |
| value.remove_prefix(1); |
| } |
| while (!value.empty() && std::isspace(static_cast<unsigned char>(value.back()))) { |
| value.remove_suffix(1); |
| } |
| return value; |
| } |
| |
| Status parse_uint64_value(std::string_view text, const std::string& field_name, uint64_t& value) { |
| text = trim_ascii(text); |
| if (text.empty()) { |
| return Status::InvalidArgument("{} is empty", field_name); |
| } |
| auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value, 10); |
| if (error != std::errc() || end != text.data() + text.size()) { |
| return Status::InvalidArgument("invalid decimal {}: '{}'", field_name, text); |
| } |
| return Status::OK(); |
| } |
| |
| Status parse_page_range_token(std::string_view token, RawPageRange& range) { |
| token = trim_ascii(token); |
| size_t separator = token.find(':'); |
| if (separator == std::string_view::npos || |
| token.find(':', separator + 1) != std::string_view::npos) { |
| return Status::InvalidArgument("invalid page range '{}', expected OFFSET:SIZE", token); |
| } |
| RETURN_IF_ERROR(parse_uint64_value(token.substr(0, separator), "page offset", range.offset)); |
| RETURN_IF_ERROR(parse_uint64_value(token.substr(separator + 1), "page size", range.size)); |
| return Status::OK(); |
| } |
| |
| Status parse_inline_page_ranges(const std::string& input, std::vector<RawPageRange>& ranges) { |
| size_t begin = 0; |
| while (begin <= input.size()) { |
| size_t end = input.find(',', begin); |
| std::string_view token(input.data() + begin, |
| (end == std::string::npos ? input.size() : end) - begin); |
| if (trim_ascii(token).empty()) { |
| return Status::InvalidArgument("page_ranges contains an empty range"); |
| } |
| RawPageRange range; |
| RETURN_IF_ERROR(parse_page_range_token(token, range)); |
| ranges.push_back(range); |
| if (end == std::string::npos) { |
| break; |
| } |
| begin = end + 1; |
| } |
| return Status::OK(); |
| } |
| |
| Status parse_page_ranges_file(const std::string& file_name, std::vector<RawPageRange>& ranges) { |
| std::ifstream input(file_name); |
| if (!input.is_open()) { |
| return Status::IOError("failed to open page ranges file {}", file_name); |
| } |
| |
| std::string line; |
| uint64_t line_number = 0; |
| while (std::getline(input, line)) { |
| ++line_number; |
| size_t comment = line.find('#'); |
| std::string_view content(line.data(), comment == std::string::npos ? line.size() : comment); |
| content = trim_ascii(content); |
| if (content.empty()) { |
| continue; |
| } |
| |
| std::istringstream line_stream {std::string(content)}; |
| std::string offset_text; |
| std::string size_text; |
| std::string extra; |
| if (!(line_stream >> offset_text >> size_text) || (line_stream >> extra)) { |
| return Status::InvalidArgument("invalid page ranges file line {}: expected OFFSET SIZE", |
| line_number); |
| } |
| RawPageRange range; |
| RETURN_IF_ERROR(parse_uint64_value(offset_text, "page offset", range.offset)); |
| RETURN_IF_ERROR(parse_uint64_value(size_text, "page size", range.size)); |
| ranges.push_back(range); |
| } |
| if (!input.eof()) { |
| return Status::IOError("failed while reading page ranges file {}", file_name); |
| } |
| return Status::OK(); |
| } |
| |
| Status load_page_ranges(std::vector<RawPageRange>& ranges) { |
| bool has_inline_ranges = !FLAGS_page_ranges.empty(); |
| bool has_ranges_file = !FLAGS_page_ranges_file.empty(); |
| if (has_inline_ranges == has_ranges_file) { |
| return Status::InvalidArgument( |
| "check_page_crc requires exactly one of page_ranges or page_ranges_file"); |
| } |
| if (has_inline_ranges) { |
| RETURN_IF_ERROR(parse_inline_page_ranges(FLAGS_page_ranges, ranges)); |
| } else { |
| RETURN_IF_ERROR(parse_page_ranges_file(FLAGS_page_ranges_file, ranges)); |
| } |
| if (ranges.empty()) { |
| return Status::InvalidArgument("no page ranges were provided"); |
| } |
| return Status::OK(); |
| } |
| |
| Status parse_page_output_mode(PageOutputMode& mode) { |
| if (FLAGS_page_output == "all") { |
| mode = PageOutputMode::ALL; |
| } else if (FLAGS_page_output == "errors") { |
| mode = PageOutputMode::ERRORS; |
| } else if (FLAGS_page_output == "summary") { |
| mode = PageOutputMode::SUMMARY; |
| } else { |
| return Status::InvalidArgument("page_output must be all, errors, or summary, got '{}'", |
| FLAGS_page_output); |
| } |
| return Status::OK(); |
| } |
| |
| bool should_print_page(PageOutputMode mode, const PageChecksumScanResult& result) { |
| return mode == PageOutputMode::ALL || (mode == PageOutputMode::ERRORS && !result.ok()); |
| } |
| |
| void print_page_checksum_result(uint64_t page_index, const PageChecksumScanResult& result) { |
| std::cout << "page=" << page_index << " offset=" << result.offset << " size=" << result.size |
| << " range_valid=" << (result.range_valid ? "true" : "false") |
| << " readable=" << (result.readable ? "true" : "false") |
| << " actual=" << result.actual_checksum << " expect=" << result.expected_checksum |
| << " checksum_ok=" << (result.checksum_ok ? "true" : "false") |
| << " footer_size=" << result.footer_size |
| << " footer_ok=" << (result.footer_ok ? "true" : "false") |
| << " page_type=" << PageTypePB_Name(result.page_type); |
| if (!result.error.empty()) { |
| std::cout << " error=" << result.error; |
| } |
| std::cout << std::endl; |
| } |
| |
| void print_page_crc_summary(const std::string& mode, const PageCrcSummary& summary, |
| const std::string& status) { |
| std::cout << "\n=== Page CRC Summary ===" << std::endl; |
| std::cout << "Mode: " << mode << std::endl; |
| std::cout << "Pages Checked: " << summary.pages_checked << std::endl; |
| std::cout << "Valid: " << summary.valid << std::endl; |
| std::cout << "Bad CRC: " << summary.bad_crc << std::endl; |
| std::cout << "Bad Footer: " << summary.bad_footer << std::endl; |
| std::cout << "Unreadable: " << summary.unreadable << std::endl; |
| std::cout << "Invalid Range: " << summary.invalid_range << std::endl; |
| std::cout << "Status: " << status << std::endl; |
| } |
| |
| Status open_page_crc_file(FileReaderSPtr& file_reader) { |
| Status status = doris::io::global_local_filesystem()->open_file(FLAGS_file, &file_reader); |
| if (!status.ok()) { |
| status.prepend("failed to open page CRC input " + FLAGS_file + ": "); |
| } |
| return status; |
| } |
| |
| Status check_page_crc_ranges() { |
| PageOutputMode output_mode; |
| RETURN_IF_ERROR(parse_page_output_mode(output_mode)); |
| std::vector<RawPageRange> ranges; |
| RETURN_IF_ERROR(load_page_ranges(ranges)); |
| |
| FileReaderSPtr file_reader; |
| RETURN_IF_ERROR(open_page_crc_file(file_reader)); |
| std::cout << "=== Page CRC Check ===" << std::endl; |
| std::cout << "File: " << FLAGS_file << std::endl; |
| std::cout << "File Size: " << file_reader->size() << std::endl; |
| std::cout << "Ranges: " << ranges.size() << std::endl; |
| |
| PageCrcSummary summary; |
| for (size_t i = 0; i < ranges.size(); ++i) { |
| PageChecksumScanResult result = |
| check_page_checksum(file_reader, ranges[i].offset, ranges[i].size); |
| summary.add(result); |
| if (should_print_page(output_mode, result)) { |
| print_page_checksum_result(i, result); |
| } |
| } |
| |
| print_page_crc_summary("ranges", summary, summary.ok() ? "OK" : "CORRUPTION"); |
| if (!summary.ok()) { |
| return Status::Corruption( |
| "page CRC check failed: valid={}, checked={}, bad_crc={}, bad_footer={}, " |
| "unreadable={}, invalid_range={}", |
| summary.valid, summary.pages_checked, summary.bad_crc, summary.bad_footer, |
| summary.unreadable, summary.invalid_range); |
| } |
| return Status::OK(); |
| } |
| |
| Status find_next_crc_page(const FileReaderSPtr& file_reader, uint64_t page_offset, |
| uint64_t scan_end, PageChecksumScanResult& result, bool& found) { |
| constexpr size_t scan_chunk_size = 1024 * 1024; |
| std::vector<char> buffer(scan_chunk_size + 4); |
| uint64_t cursor = page_offset; |
| uint32_t checksum = 0; |
| found = false; |
| |
| while (scan_end - cursor >= 5) { |
| size_t bytes_to_process = |
| static_cast<size_t>(std::min<uint64_t>(scan_chunk_size, scan_end - cursor - 4)); |
| size_t bytes_to_read = bytes_to_process + 4; |
| RETURN_IF_ERROR(read_file_exact(file_reader, cursor, buffer.data(), bytes_to_read)); |
| for (size_t i = 0; i < bytes_to_process; ++i) { |
| checksum = crc32c::Extend(checksum, reinterpret_cast<const uint8_t*>(buffer.data() + i), |
| 1); |
| uint64_t payload_size = cursor + i - page_offset + 1; |
| if (payload_size < 4) { |
| continue; |
| } |
| uint32_t expected = doris::decode_fixed32_le( |
| reinterpret_cast<const uint8_t*>(buffer.data() + i + 1)); |
| if (checksum != expected) { |
| continue; |
| } |
| |
| PageChecksumScanResult candidate = |
| check_page_checksum(file_reader, page_offset, payload_size + 4); |
| if (candidate.ok()) { |
| result = std::move(candidate); |
| found = true; |
| return Status::OK(); |
| } |
| } |
| cursor += bytes_to_process; |
| } |
| return Status::OK(); |
| } |
| |
| Status scan_page_crc_range() { |
| PageOutputMode output_mode; |
| RETURN_IF_ERROR(parse_page_output_mode(output_mode)); |
| if (gflags::GetCommandLineFlagInfoOrDie("page_scan_start").is_default || |
| gflags::GetCommandLineFlagInfoOrDie("page_scan_end").is_default) { |
| return Status::InvalidArgument( |
| "scan_page_crc requires explicit page_scan_start and page_scan_end"); |
| } |
| if (FLAGS_page_scan_end <= FLAGS_page_scan_start) { |
| return Status::InvalidArgument("page_scan_end {} must be greater than page_scan_start {}", |
| FLAGS_page_scan_end, FLAGS_page_scan_start); |
| } |
| |
| FileReaderSPtr file_reader; |
| RETURN_IF_ERROR(open_page_crc_file(file_reader)); |
| if (FLAGS_page_scan_end > file_reader->size()) { |
| return Status::InvalidArgument("page_scan_end {} exceeds file size {}", FLAGS_page_scan_end, |
| file_reader->size()); |
| } |
| |
| std::cout << "=== Page CRC Scan ===" << std::endl; |
| std::cout << "File: " << FLAGS_file << std::endl; |
| std::cout << "File Size: " << file_reader->size() << std::endl; |
| std::cout << "Range: [" << FLAGS_page_scan_start << "," << FLAGS_page_scan_end << ")" |
| << std::endl; |
| |
| PageCrcSummary summary; |
| uint64_t page_offset = FLAGS_page_scan_start; |
| uint64_t page_index = 0; |
| while (page_offset < FLAGS_page_scan_end) { |
| PageChecksumScanResult result; |
| bool found = false; |
| RETURN_IF_ERROR( |
| find_next_crc_page(file_reader, page_offset, FLAGS_page_scan_end, result, found)); |
| if (!found) { |
| if (output_mode != PageOutputMode::SUMMARY) { |
| std::cout << "no_page_boundary offset=" << page_offset |
| << " remaining=" << (FLAGS_page_scan_end - page_offset) << std::endl; |
| } |
| print_page_crc_summary("scan", summary, "CORRUPTION"); |
| return Status::Corruption("no page boundary at offset {}, remaining={}", page_offset, |
| FLAGS_page_scan_end - page_offset); |
| } |
| |
| summary.add(result); |
| if (should_print_page(output_mode, result)) { |
| print_page_checksum_result(page_index, result); |
| } |
| page_offset += result.size; |
| ++page_index; |
| } |
| |
| print_page_crc_summary("scan", summary, summary.ok() ? "OK" : "CORRUPTION"); |
| if (!summary.ok()) { |
| return Status::Corruption("page CRC scan failed"); |
| } |
| return Status::OK(); |
| } |
| |
| // Keep the forensic scan linear so every counter and printed example follows page order. |
| // NOLINTNEXTLINE(readability-function-size) |
| Status print_column_page_checksums(const std::shared_ptr<ColumnReader>& column_reader, |
| const doris::segment_v2::ColumnMetaPB& column_meta, |
| const FileReaderSPtr& file_reader, int indent_level) { |
| std::string indent(indent_level * 2, ' '); |
| doris::OlapReaderStatistics stats; |
| OrdinalIndexReader* ordinal_index = nullptr; |
| Status status = column_reader->get_ordinal_index_reader(ordinal_index, &stats); |
| if (!status.ok()) { |
| status.prepend("failed to load ordinal index for column " + |
| std::to_string(column_meta.column_id()) + ": "); |
| return status; |
| } |
| |
| constexpr uint64_t s3_part_size = 5 * 1024 * 1024; |
| int valid_pages = 0; |
| int bad_pages = 0; |
| int unreadable_pages = 0; |
| int noncontiguous_pages = 0; |
| int multipart_crossing_pages = 0; |
| bool dict_bad = false; |
| bool dict_unreadable = false; |
| uint64_t span_begin = file_reader->size(); |
| uint64_t span_end = 0; |
| uint64_t previous_end = 0; |
| bool have_previous = false; |
| int bad_examples = 0; |
| |
| std::cout << indent << "Page checksum scan:" << std::endl; |
| for (auto iter = ordinal_index->begin(); iter.valid(); iter.next()) { |
| const PagePointer& pp = iter.page(); |
| span_begin = std::min(span_begin, pp.offset); |
| span_end = std::max(span_end, pp.offset + pp.size); |
| if (have_previous && pp.offset != previous_end) { |
| ++noncontiguous_pages; |
| } |
| previous_end = pp.offset + pp.size; |
| have_previous = true; |
| |
| bool crosses_part = |
| pp.size > 0 && pp.offset / s3_part_size != (pp.offset + pp.size - 1) / s3_part_size; |
| if (crosses_part) { |
| ++multipart_crossing_pages; |
| } |
| |
| PageChecksumScanResult page = scan_page_checksum(file_reader, pp); |
| if (!page.readable) { |
| ++unreadable_pages; |
| } else if (page.ok()) { |
| ++valid_pages; |
| } else { |
| ++bad_pages; |
| } |
| |
| if (iter.page_index() < 2 || crosses_part || (!page.ok() && bad_examples < 3)) { |
| std::cout << indent << " page=" << iter.page_index() |
| << " ordinals=" << iter.first_ordinal() << ".." << iter.last_ordinal() |
| << " offset=" << pp.offset << " size=" << pp.size |
| << " readable=" << (page.readable ? "true" : "false") |
| << " checksum_ok=" << (page.checksum_ok ? "true" : "false") |
| << " actual=" << page.actual_checksum << " expect=" << page.expected_checksum |
| << " footer_size=" << page.footer_size |
| << " footer_ok=" << (page.footer_ok ? "true" : "false") |
| << " crosses_s3_part=" << (crosses_part ? "true" : "false") << std::endl; |
| if (!page.ok()) { |
| ++bad_examples; |
| } |
| } |
| } |
| |
| if (column_meta.has_dict_page()) { |
| PagePointer dict_page(column_meta.dict_page()); |
| PageChecksumScanResult page = scan_page_checksum(file_reader, dict_page); |
| std::cout << indent << " dict offset=" << dict_page.offset << " size=" << dict_page.size |
| << " readable=" << (page.readable ? "true" : "false") |
| << " checksum_ok=" << (page.checksum_ok ? "true" : "false") |
| << " actual=" << page.actual_checksum << " expect=" << page.expected_checksum |
| << " footer_size=" << page.footer_size |
| << " footer_ok=" << (page.footer_ok ? "true" : "false") << std::endl; |
| if (!page.readable) { |
| dict_unreadable = true; |
| } else if (!page.ok()) { |
| dict_bad = true; |
| } |
| } |
| |
| std::cout << indent << " summary pages=" << ordinal_index->num_data_pages() |
| << " valid=" << valid_pages << " bad=" << bad_pages |
| << " unreadable=" << unreadable_pages << " span=[" << span_begin << "," << span_end |
| << ") noncontiguous=" << noncontiguous_pages |
| << " crosses_s3_part=" << multipart_crossing_pages << std::endl; |
| |
| if (bad_pages > 0 || unreadable_pages > 0 || dict_bad || dict_unreadable) { |
| return Status::Corruption( |
| "page checksum scan failed for column {}: data_bad={}, data_unreadable={}, " |
| "dict_bad={}, dict_unreadable={}", |
| column_meta.column_id(), bad_pages, unreadable_pages, dict_bad, dict_unreadable); |
| } |
| return Status::OK(); |
| } |
| |
| Status validate_segment_data_options(uint64_t num_segment_rows, uint64_t* rows_to_read) { |
| if (FLAGS_rows < -1) { |
| return Status::InvalidArgument("rows must be -1 or non-negative, got {}", FLAGS_rows); |
| } |
| if (FLAGS_batch_rows == 0 || FLAGS_batch_rows > std::numeric_limits<size_t>::max()) { |
| return Status::InvalidArgument("batch_rows must be in [1, {}], got {}", |
| std::numeric_limits<size_t>::max(), FLAGS_batch_rows); |
| } |
| if (FLAGS_row_start > num_segment_rows) { |
| return Status::InvalidArgument("row_start {} exceeds segment row count {}", FLAGS_row_start, |
| num_segment_rows); |
| } |
| |
| uint64_t remaining_rows = num_segment_rows - FLAGS_row_start; |
| *rows_to_read = FLAGS_rows == -1 |
| ? remaining_rows |
| : std::min<uint64_t>(static_cast<uint64_t>(FLAGS_rows), remaining_rows); |
| return Status::OK(); |
| } |
| |
| // Read and print column data values. Keep reader setup and the bounded decode loop together so |
| // failures retain exact column/row context. |
| // NOLINTNEXTLINE(readability-function-size) |
| Status print_column_data_values(const doris::segment_v2::ColumnMetaPB& column_meta, |
| const FileReaderSPtr& file_reader, uint64_t num_segment_rows, |
| uint64_t row_start, uint64_t rows_to_read, int indent_level) { |
| std::string indent(indent_level * 2, ' '); |
| |
| auto field_type = static_cast<doris::FieldType>(column_meta.type()); |
| |
| if (!doris::is_scalar_type(field_type)) { |
| return Status::NotSupported("cannot read complex column {} as scalar data", |
| column_meta.column_id()); |
| } |
| |
| // Create a virtual TabletColumn for the column |
| doris::TabletColumn tablet_column; |
| tablet_column.set_aggregation_method( |
| doris::FieldAggregationMethod::OLAP_FIELD_AGGREGATION_NONE); |
| tablet_column.set_type(field_type); |
| tablet_column.set_is_nullable(column_meta.is_nullable()); |
| tablet_column.set_length(0); // Default length |
| tablet_column.set_unique_id(column_meta.column_id()); |
| |
| // Create column reader |
| ColumnReaderOptions reader_opts; |
| reader_opts.verify_checksum = FLAGS_verify_checksum; |
| |
| std::shared_ptr<ColumnReader> column_reader; |
| Status status = ColumnReader::create(reader_opts, column_meta, num_segment_rows, file_reader, |
| &column_reader); |
| if (!status.ok()) { |
| status.prepend("failed to create reader for column " + |
| std::to_string(column_meta.column_id()) + ": "); |
| return status; |
| } |
| |
| // Create column iterator |
| ColumnIteratorUPtr iterator; |
| status = column_reader->new_iterator(&iterator, &tablet_column); |
| if (!status.ok()) { |
| status.prepend("failed to create iterator for column " + |
| std::to_string(column_meta.column_id()) + ": "); |
| return status; |
| } |
| |
| // Initialize iterator |
| ColumnIteratorOptions iter_opts; |
| iter_opts.file_reader = file_reader.get(); |
| doris::OlapReaderStatistics stats; // Dummy statistics |
| iter_opts.stats = &stats; |
| |
| status = iterator->init(iter_opts); |
| if (!status.ok()) { |
| status.prepend("failed to initialize iterator for column " + |
| std::to_string(column_meta.column_id()) + ": "); |
| return status; |
| } |
| |
| if (FLAGS_scan_segment_pages) { |
| RETURN_IF_ERROR( |
| print_column_page_checksums(column_reader, column_meta, file_reader, indent_level)); |
| } |
| |
| if (rows_to_read == 0) { |
| if (FLAGS_check_only) { |
| std::cout << indent << "Data check: rows=0 range=[" << row_start << "," << row_start |
| << ") batches=0 verify_checksum=" |
| << (FLAGS_verify_checksum ? "true" : "false") << " status=OK" << std::endl; |
| } else { |
| std::cout << indent << "Data Values (rows [" << row_start << "," << row_start << ") of " |
| << num_segment_rows << "):" << std::endl; |
| } |
| return Status::OK(); |
| } |
| |
| status = iterator->seek_to_ordinal(row_start); |
| if (!status.ok()) { |
| status.prepend("failed to seek column " + std::to_string(column_meta.column_id()) + |
| " to row " + std::to_string(row_start) + ": "); |
| return status; |
| } |
| |
| auto data_type = doris::DataTypeFactory::instance().create_data_type(column_meta); |
| if (!data_type) { |
| return Status::InternalError("failed to create data type for column {}, field type {}", |
| column_meta.column_id(), static_cast<int>(field_type)); |
| } |
| |
| if (!FLAGS_check_only) { |
| std::cout << indent << "Data Values (rows [" << row_start << "," |
| << (row_start + rows_to_read) << ") of " << num_segment_rows << "):" << std::endl; |
| } |
| |
| uint64_t decoded_rows = 0; |
| uint64_t batches = 0; |
| while (decoded_rows < rows_to_read) { |
| uint64_t current_row = row_start + decoded_rows; |
| size_t requested_rows = static_cast<size_t>( |
| std::min<uint64_t>(FLAGS_batch_rows, rows_to_read - decoded_rows)); |
| size_t rows_read = requested_rows; |
| doris::MutableColumnPtr dst_column = data_type->create_column(); |
| |
| status = iterator->next_batch(&rows_read, dst_column); |
| if (!status.ok()) { |
| status.prepend("failed to read column " + std::to_string(column_meta.column_id()) + |
| " at row " + std::to_string(current_row) + ": "); |
| return status; |
| } |
| if (rows_read == 0) { |
| return Status::Corruption( |
| "column {} reached an unexpected EOF at row {}, expected range end {}", |
| column_meta.column_id(), current_row, row_start + rows_to_read); |
| } |
| if (rows_read > requested_rows || dst_column->size() != rows_read) { |
| return Status::Corruption( |
| "column {} returned an invalid batch at row {}: requested={}, read={}, " |
| "values={}", |
| column_meta.column_id(), current_row, requested_rows, rows_read, |
| dst_column->size()); |
| } |
| |
| if (!FLAGS_check_only) { |
| for (size_t i = 0; i < rows_read; ++i) { |
| std::cout << indent << " [" << (current_row + i) << "] "; |
| if (column_meta.is_nullable()) { |
| const auto& nullable_col = |
| assert_cast<const doris::ColumnNullable&>(*dst_column); |
| if (nullable_col.is_null_at(i)) { |
| std::cout << "NULL"; |
| } else { |
| const doris::IColumn& nested_col = nullable_col.get_nested_column(); |
| std::cout << format_column_value(nested_col, i, field_type); |
| } |
| } else { |
| std::cout << format_column_value(*dst_column, i, field_type); |
| } |
| std::cout << std::endl; |
| } |
| } |
| |
| decoded_rows += rows_read; |
| ++batches; |
| } |
| |
| if (FLAGS_check_only) { |
| std::cout << indent << "Data check: rows=" << decoded_rows << " range=[" << row_start << "," |
| << (row_start + decoded_rows) << ") batches=" << batches |
| << " verify_checksum=" << (FLAGS_verify_checksum ? "true" : "false") |
| << " status=OK" << std::endl; |
| } else if (rows_to_read < num_segment_rows) { |
| std::cout << indent << " ... (" << (num_segment_rows - rows_to_read) |
| << " rows outside selected range)" << std::endl; |
| } |
| |
| return Status::OK(); |
| } |
| |
| // Helper function to print column metadata. The output intentionally mirrors protobuf field order |
| // for forensic readability. |
| // NOLINTNEXTLINE(readability-function-size) |
| Status print_column_meta(const doris::segment_v2::ColumnMetaPB& column_meta, |
| const FileReaderSPtr& file_reader, uint64_t num_segment_rows, |
| uint64_t row_start, uint64_t rows_to_read, int indent_level) { |
| std::string indent(indent_level * 2, ' '); |
| std::string column_name; |
| if (column_meta.has_column_path_info() && column_meta.column_path_info().has_path()) { |
| column_name = column_meta.column_path_info().path(); |
| } else { |
| column_name = "column_id_" + std::to_string(column_meta.column_id()); |
| } |
| |
| auto field_type = static_cast<doris::FieldType>(column_meta.type()); |
| std::cout << indent << "=== " << column_name << ": type=" << get_field_type_string(field_type) |
| << ", nullable=" << (column_meta.is_nullable() ? "true" : "false") |
| << ", encoding=" << get_encoding_string(column_meta.encoding()) |
| << " ===" << std::endl; |
| |
| // Print size info |
| if (column_meta.has_compressed_data_bytes()) { |
| std::cout << indent << "Data Size (Compressed): " << column_meta.compressed_data_bytes() |
| << " bytes" << std::endl; |
| } |
| if (column_meta.has_uncompressed_data_bytes()) { |
| std::cout << indent << "Data Size (Uncompressed): " << column_meta.uncompressed_data_bytes() |
| << " bytes" << std::endl; |
| } |
| if (column_meta.has_raw_data_bytes()) { |
| std::cout << indent << "Raw Data Size: " << column_meta.raw_data_bytes() << " bytes" |
| << std::endl; |
| } |
| |
| // Print dict page info |
| if (column_meta.has_dict_page()) { |
| const auto& dict_page = column_meta.dict_page(); |
| std::cout << indent << "Dictionary Page: offset=" << dict_page.offset() |
| << ", size=" << dict_page.size() << " bytes" << std::endl; |
| } |
| |
| // Print indexes info |
| if (column_meta.indexes_size() > 0) { |
| std::cout << indent << "Indexes: "; |
| for (int i = 0; i < column_meta.indexes_size(); ++i) { |
| if (i > 0) { |
| std::cout << ", "; |
| } |
| const auto& index_meta = column_meta.indexes(i); |
| if (index_meta.has_type()) { |
| switch (index_meta.type()) { |
| case doris::segment_v2::ORDINAL_INDEX: |
| std::cout << "ORDINAL"; |
| break; |
| case doris::segment_v2::ZONE_MAP_INDEX: |
| std::cout << "ZONE_MAP"; |
| break; |
| case doris::segment_v2::BLOOM_FILTER_INDEX: |
| std::cout << "BLOOM_FILTER"; |
| break; |
| case doris::segment_v2::BITMAP_INDEX: |
| std::cout << "BITMAP"; |
| break; |
| default: |
| std::cout << "UNKNOWN"; |
| break; |
| } |
| } |
| } |
| std::cout << std::endl; |
| } |
| |
| // Handle complex types recursively |
| if (column_meta.children_columns_size() > 0) { |
| if (FLAGS_check_only) { |
| return Status::NotSupported( |
| "check_only does not yet support complex column {} with {} children", |
| column_meta.column_id(), column_meta.children_columns_size()); |
| } |
| std::cout << indent << "Sub-columns: " << column_meta.children_columns_size() << std::endl; |
| for (int i = 0; i < column_meta.children_columns_size(); ++i) { |
| RETURN_IF_ERROR(print_column_meta(column_meta.children_columns(i), file_reader, |
| num_segment_rows, row_start, rows_to_read, |
| indent_level + 1)); |
| } |
| return Status::OK(); |
| } |
| |
| // Print column data values for scalar types |
| if (doris::is_scalar_type(field_type)) { |
| return print_column_data_values(column_meta, file_reader, num_segment_rows, row_start, |
| rows_to_read, indent_level); |
| } else { |
| return Status::NotSupported("cannot display values for column {} with type {}", |
| column_meta.column_id(), get_field_type_string(field_type)); |
| } |
| } |
| |
| // Register hijacked accessors |
| ACCESS_PRIVATE_FIELD(ExecEnv_encoding_info_resolver, doris::ExecEnv, |
| doris::segment_v2::EncodingInfoResolver*, _encoding_info_resolver); |
| ACCESS_PRIVATE_FIELD(ExecEnv_orphan_mem_tracker, doris::ExecEnv, |
| std::shared_ptr<doris::MemTrackerLimiter>, _orphan_mem_tracker); |
| ACCESS_PRIVATE_STATIC_FIELD(ExecEnv_tracking_memory, doris::ExecEnv, std::atomic_bool, |
| _s_tracking_memory); |
| |
| // Keep report sections in execution order so a failure never prints a misleading final summary. |
| // NOLINTNEXTLINE(readability-function-size) |
| Status show_segment_data(const std::string& file_name) { |
| // Initialize ExecEnv components needed for ColumnReader |
| // Use macro to access private members temporarily |
| auto* exec_env = doris::ExecEnv::GetInstance(); |
| |
| auto resolver = GET_PRIVATE_FIELD(ExecEnv_encoding_info_resolver); |
| auto mem_tracker = GET_PRIVATE_FIELD(ExecEnv_orphan_mem_tracker); |
| auto tracking_memory = GET_PRIVATE_STATIC_FIELD(ExecEnv_tracking_memory); |
| // Initialize encoding info resolver for ColumnReader |
| if (exec_env->*resolver == nullptr) { |
| exec_env->*resolver = new doris::segment_v2::EncodingInfoResolver(); |
| } |
| // Initialize mem tracker limiter pool and orphan mem tracker for ThreadMemTrackerMgr |
| if (exec_env->mem_tracker_limiter_pool.empty()) { |
| exec_env->mem_tracker_limiter_pool.resize(doris::MEM_TRACKER_GROUP_NUM, |
| doris::TrackerLimiterGroup()); |
| tracking_memory->store(true, std::memory_order_release); |
| exec_env->*mem_tracker = doris::MemTrackerLimiter::create_shared( |
| doris::MemTrackerLimiter::Type::GLOBAL, "Orphan"); |
| } |
| |
| doris::io::FileReaderSPtr file_reader; |
| Status status = doris::io::global_local_filesystem()->open_file(file_name, &file_reader); |
| if (!status.ok()) { |
| status.prepend("failed to open segment file " + file_name + ": "); |
| return status; |
| } |
| |
| SegmentFooterPB footer; |
| status = get_segment_footer(file_reader.get(), &footer); |
| if (!status.ok()) { |
| status.prepend("failed to read segment footer from " + file_name + ": "); |
| return status; |
| } |
| |
| uint64_t rows_to_read = 0; |
| RETURN_IF_ERROR(validate_segment_data_options(footer.num_rows(), &rows_to_read)); |
| |
| // Print basic info |
| std::cout << "\n=== Segment File Info ===" << std::endl; |
| std::cout << "File: " << file_name << std::endl; |
| std::cout << "Num Rows: " << footer.num_rows() << std::endl; |
| std::cout << "Num Columns: " << footer.columns_size() << std::endl; |
| std::cout << "Compression: " << get_compression_string(footer.compress_type()) << std::endl; |
| std::cout << "Selected Row Range: [" << FLAGS_row_start << "," |
| << (FLAGS_row_start + rows_to_read) << ")" << std::endl; |
| std::cout << "Check Only: " << (FLAGS_check_only ? "true" : "false") << std::endl; |
| std::cout << "Verify Checksum: " << (FLAGS_verify_checksum ? "true" : "false") << std::endl; |
| if (footer.has_version()) { |
| std::cout << "Version: " << footer.version() << std::endl; |
| } |
| std::cout << std::endl; |
| |
| // Collect statistics |
| uint64_t total_compressed_data_bytes = 0; |
| uint64_t total_uncompressed_data_bytes = 0; |
| uint64_t total_raw_data_bytes = 0; |
| uint32_t total_ordinal_indexes = 0; |
| uint32_t total_zone_map_indexes = 0; |
| uint32_t total_bloom_filter_indexes = 0; |
| uint32_t columns_with_dict = 0; |
| |
| // Print each column |
| for (int i = 0; i < footer.columns_size(); ++i) { |
| const auto& column_meta = footer.columns(i); |
| RETURN_IF_ERROR(print_column_meta(column_meta, file_reader, footer.num_rows(), |
| FLAGS_row_start, rows_to_read, 0)); |
| |
| // Collect statistics |
| if (column_meta.has_compressed_data_bytes()) { |
| total_compressed_data_bytes += column_meta.compressed_data_bytes(); |
| } |
| if (column_meta.has_uncompressed_data_bytes()) { |
| total_uncompressed_data_bytes += column_meta.uncompressed_data_bytes(); |
| } |
| if (column_meta.has_raw_data_bytes()) { |
| total_raw_data_bytes += column_meta.raw_data_bytes(); |
| } |
| |
| // Count indexes |
| for (int j = 0; j < column_meta.indexes_size(); ++j) { |
| const auto& index_meta = column_meta.indexes(j); |
| if (index_meta.has_type()) { |
| switch (index_meta.type()) { |
| case doris::segment_v2::ORDINAL_INDEX: |
| total_ordinal_indexes++; |
| break; |
| case doris::segment_v2::ZONE_MAP_INDEX: |
| total_zone_map_indexes++; |
| break; |
| case doris::segment_v2::BLOOM_FILTER_INDEX: |
| total_bloom_filter_indexes++; |
| break; |
| default: |
| break; |
| } |
| } |
| } |
| |
| if (column_meta.has_dict_page()) { |
| columns_with_dict++; |
| } |
| |
| std::cout << std::endl; |
| } |
| |
| // Print statistics |
| std::cout << "\n=== Statistics ===" << std::endl; |
| uint32_t total_indexes = |
| total_ordinal_indexes + total_zone_map_indexes + total_bloom_filter_indexes; |
| |
| std::cout << "Total Columns: " << footer.columns_size() << std::endl; |
| std::cout << "Columns with Dictionary: " << columns_with_dict << std::endl; |
| std::cout << "Total Indexes: " << total_indexes << std::endl; |
| std::cout << " - Ordinal Indexes: " << total_ordinal_indexes << std::endl; |
| std::cout << " - Zone Map Indexes: " << total_zone_map_indexes << std::endl; |
| std::cout << " - Bloom Filter Indexes: " << total_bloom_filter_indexes << std::endl; |
| std::cout << "Total Data Size (Compressed): " << total_compressed_data_bytes << " bytes (" |
| << std::fixed << std::setprecision(2) << (total_compressed_data_bytes / 1024.0) |
| << " KB)" << std::endl; |
| std::cout << "Total Data Size (Uncompressed): " << total_uncompressed_data_bytes << " bytes (" |
| << std::fixed << std::setprecision(2) << (total_uncompressed_data_bytes / 1024.0) |
| << " KB)" << std::endl; |
| std::cout << "Total Raw Data Size: " << total_raw_data_bytes << " bytes (" << std::fixed |
| << std::setprecision(2) << (total_raw_data_bytes / 1024.0) << " KB)" << std::endl; |
| if (footer.has_index_footprint()) { |
| std::cout << "Index Footprint: " << footer.index_footprint() << " bytes (" << std::fixed |
| << std::setprecision(2) << (footer.index_footprint() / 1024.0) << " KB)" |
| << std::endl; |
| } |
| if (footer.has_data_footprint()) { |
| std::cout << "Data Footprint: " << footer.data_footprint() << " bytes (" << std::fixed |
| << std::setprecision(2) << (footer.data_footprint() / 1024.0) << " KB)" |
| << std::endl; |
| } |
| |
| std::cout << "\n=== Data Read Summary ===" << std::endl; |
| std::cout << "Columns Checked: " << footer.columns_size() << std::endl; |
| std::cout << "Rows Per Column: " << rows_to_read << std::endl; |
| std::cout << "Row Range: [" << FLAGS_row_start << "," << (FLAGS_row_start + rows_to_read) << ")" |
| << std::endl; |
| std::cout << "Check Only: " << (FLAGS_check_only ? "true" : "false") << std::endl; |
| std::cout << "Verify Checksum: " << (FLAGS_verify_checksum ? "true" : "false") << std::endl; |
| std::cout << "Status: OK" << std::endl; |
| return Status::OK(); |
| } |
| |
| void init_common_components() { |
| // init meta_tool.log to current dir |
| if (doris::config::sys_log_dir == "") { |
| doris::config::sys_log_dir = "."; |
| } |
| if (doris::config::sys_log_level == "") { |
| doris::config::sys_log_level = "INFO"; |
| } |
| if (doris::config::sys_log_roll_mode == "") { |
| doris::config::sys_log_roll_mode = "SIZE-MB-1024"; |
| } |
| FLAGS_log_dir = doris::config::sys_log_dir; |
| if (!doris::init_glog("meta_tool")) { |
| fprintf(stderr, "init glog failed.\n"); |
| } |
| |
| doris::ExecEnv::GetInstance()->init_mem_tracker(); |
| doris::ExecEnv::GetInstance()->set_cache_manager(doris::CacheManager::create_global_instance()); |
| doris::ExecEnv::GetInstance()->set_tablet_schema_cache( |
| doris::TabletSchemaCache::create_global_schema_cache( |
| doris::config::tablet_schema_cache_capacity)); |
| doris::ExecEnv::GetInstance()->set_tablet_column_object_pool( |
| doris::TabletColumnObjectPool::create_global_column_cache( |
| doris::config::tablet_schema_cache_capacity)); |
| } |
| |
| void gen_empty_segment() { |
| std::string output_path = FLAGS_output_path.empty() ? "." : FLAGS_output_path; |
| |
| // Create output file path |
| std::string file_path = output_path + "/empty.dat"; |
| |
| // Open file for writing |
| std::ofstream out_file(file_path, std::ios::binary); |
| if (!out_file.is_open()) { |
| std::cout << "failed to open output file: " << file_path << std::endl; |
| return; |
| } |
| |
| // 1. Build empty short key index page |
| std::vector<Slice> index_body; |
| segment_v2::PageFooterPB index_footer; |
| index_footer.set_type(segment_v2::SHORT_KEY_PAGE); |
| index_footer.set_uncompressed_size(0); // empty body |
| |
| segment_v2::ShortKeyFooterPB* sk_footer = index_footer.mutable_short_key_page_footer(); |
| sk_footer->set_num_items(0); // 0 keys |
| sk_footer->set_key_bytes(0); // empty key buffer |
| sk_footer->set_offset_bytes(0); // empty offset buffer |
| sk_footer->set_segment_id(0); |
| sk_footer->set_num_rows_per_block(FLAGS_num_rows_per_block); |
| sk_footer->set_num_segment_rows(0); |
| |
| // Empty key and offset buffers |
| std::string key_buf; |
| std::string offset_buf; |
| |
| index_body.push_back(Slice(key_buf.data(), key_buf.size())); |
| index_body.push_back(Slice(offset_buf.data(), offset_buf.size())); |
| |
| // Serialize index footer |
| std::string index_footer_buf; |
| index_footer.SerializeToString(&index_footer_buf); |
| doris::put_fixed32_le(&index_footer_buf, static_cast<uint32_t>(index_footer_buf.size())); |
| index_body.push_back(Slice(index_footer_buf.data(), index_footer_buf.size())); |
| |
| // Calculate checksum for index page |
| uint32_t index_checksum = 0; |
| for (const auto& slice : index_body) { |
| index_checksum = crc32c::Extend(index_checksum, (const uint8_t*)slice.data, slice.size); |
| } |
| uint8_t index_checksum_buf[sizeof(uint32_t)]; |
| doris::encode_fixed32_le(index_checksum_buf, index_checksum); |
| index_body.push_back(Slice(index_checksum_buf, sizeof(uint32_t))); |
| |
| // 2. Build segment footer |
| SegmentFooterPB footer; |
| footer.set_num_rows(0); |
| |
| // Calculate total index page size |
| uint64_t index_page_size = 0; |
| for (const auto& slice : index_body) { |
| index_page_size += slice.size; |
| } |
| |
| // Set short key index page pointer |
| segment_v2::PagePointer index_pp; |
| index_pp.offset = 0; |
| index_pp.size = static_cast<uint32_t>(index_page_size); |
| index_pp.to_proto(footer.mutable_short_key_index_page()); |
| |
| // Serialize footer |
| std::string footer_buf; |
| if (!footer.SerializeToString(&footer_buf)) { |
| std::cout << "failed to serialize footer" << std::endl; |
| return; |
| } |
| |
| // 3. Write footer data to file |
| std::vector<Slice> footer_slices = {footer_buf}; |
| |
| // Footer size (4 bytes, little-endian) |
| uint32_t footer_size = static_cast<uint32_t>(footer_buf.size()); |
| uint8_t footer_size_buf[4]; |
| doris::encode_fixed32_le(footer_size_buf, footer_size); |
| footer_slices.push_back(Slice(footer_size_buf, 4)); |
| |
| // Footer checksum (4 bytes, crc32c) |
| uint32_t footer_checksum = crc32c::Crc32c(footer_buf.data(), footer_buf.size()); |
| uint8_t footer_checksum_buf[4]; |
| doris::encode_fixed32_le(footer_checksum_buf, footer_checksum); |
| footer_slices.push_back(Slice(footer_checksum_buf, 4)); |
| |
| // Magic number (4 bytes): "D0R1" |
| footer_slices.push_back( |
| Slice(doris::segment_v2::k_segment_magic, doris::segment_v2::k_segment_magic_length)); |
| |
| // Write index page first, then footer |
| for (const auto& slice : index_body) { |
| out_file.write(slice.data, slice.size); |
| } |
| |
| // Write footer |
| for (const auto& slice : footer_slices) { |
| out_file.write(slice.data, slice.size); |
| } |
| |
| out_file.close(); |
| |
| // Print summary |
| std::cout << "Generated empty segment file: " << file_path << std::endl; |
| std::cout << " - Index page size: " << index_page_size << " bytes" << std::endl; |
| std::cout << " - Footer size: " << footer_slices.size() << " slices, " |
| << (footer_buf.size() + 12) << " bytes" << std::endl; |
| std::cout << " - Total file size: " << (index_page_size + footer_buf.size() + 12) << " bytes" |
| << std::endl; |
| std::cout << " - num_rows: 0" << std::endl; |
| } |
| |
| int main(int argc, char** argv) { |
| SCOPED_INIT_THREAD_CONTEXT(); |
| std::string usage = get_usage(argv[0]); |
| gflags::SetUsageMessage(usage); |
| google::ParseCommandLineFlags(&argc, &argv, true); |
| |
| if (FLAGS_operation == "show_meta") { |
| init_common_components(); |
| show_meta(); |
| } else if (FLAGS_operation == "batch_delete_meta") { |
| std::string tablet_file; |
| Status st = |
| doris::io::global_local_filesystem()->canonicalize(FLAGS_tablet_file, &tablet_file); |
| if (!st.ok()) { |
| std::cout << "invalid tablet file: " << FLAGS_tablet_file |
| << ", error: " << st.to_string() << std::endl; |
| return -1; |
| } |
| |
| init_common_components(); |
| batch_delete_meta(tablet_file); |
| } else if (FLAGS_operation == "show_segment_footer") { |
| if (FLAGS_file == "") { |
| std::cout << "no file flag for show dict" << std::endl; |
| return -1; |
| } |
| init_common_components(); |
| show_segment_footer(FLAGS_file); |
| } else if (FLAGS_operation == "show_segment_data") { |
| if (FLAGS_file == "") { |
| std::cerr << "no file flag for show_segment_data" << std::endl; |
| return 2; |
| } |
| init_common_components(); |
| Status status = show_segment_data(FLAGS_file); |
| if (!status.ok()) { |
| std::cerr << "show_segment_data failed: " << status.to_string() << std::endl; |
| gflags::ShutDownCommandLineFlags(); |
| return status.is<ErrorCode::INVALID_ARGUMENT>() ? 2 : 1; |
| } |
| } else if (FLAGS_operation == "check_page_crc" || FLAGS_operation == "scan_page_crc") { |
| if (FLAGS_file.empty()) { |
| std::cerr << "no file flag for " << FLAGS_operation << std::endl; |
| return 2; |
| } |
| init_common_components(); |
| Status status = FLAGS_operation == "check_page_crc" ? check_page_crc_ranges() |
| : scan_page_crc_range(); |
| if (!status.ok()) { |
| std::cerr << FLAGS_operation << " failed: " << status.to_string() << std::endl; |
| gflags::ShutDownCommandLineFlags(); |
| return status.is<ErrorCode::INVALID_ARGUMENT>() ? 2 : 1; |
| } |
| } else if (FLAGS_operation == "gen_empty_segment") { |
| gen_empty_segment(); |
| } else { |
| // operations that need root path should be written here |
| std::set<std::string> valid_operations = {"get_meta", "load_meta", "delete_meta"}; |
| if (valid_operations.find(FLAGS_operation) == valid_operations.end()) { |
| std::cout << "invalid operation:" << FLAGS_operation << std::endl; |
| return -1; |
| } |
| |
| if (getenv("DORIS_HOME") == nullptr) { |
| fprintf(stderr, "you need set DORIS_HOME environment variable.\n"); |
| exit(-1); |
| } |
| |
| std::string conffile = std::string(getenv("DORIS_HOME")) + "/conf/be.conf"; |
| if (!doris::config::init(conffile.c_str(), true, true, true)) { |
| fprintf(stderr, "error read config file. \n"); |
| return -1; |
| } |
| |
| std::string custom_conffile = doris::config::custom_config_dir + "/be_custom.conf"; |
| if (!doris::config::init(custom_conffile.c_str(), true, false, false)) { |
| fprintf(stderr, "error read custom config file. \n"); |
| return -1; |
| } |
| |
| init_common_components(); |
| |
| StorageEngine engine(doris::EngineOptions {}); |
| std::unique_ptr<DataDir> data_dir; |
| Status st = init_data_dir(engine, FLAGS_root_path, &data_dir); |
| if (!st.ok()) { |
| std::cout << "invalid root path:" << FLAGS_root_path << ", error: " << st.to_string() |
| << std::endl; |
| return -1; |
| } |
| |
| if (FLAGS_operation == "get_meta") { |
| get_meta(data_dir.get()); |
| } else if (FLAGS_operation == "load_meta") { |
| load_meta(data_dir.get()); |
| } else if (FLAGS_operation == "delete_meta") { |
| delete_meta(data_dir.get()); |
| } else { |
| std::cout << "invalid operation: " << FLAGS_operation << "\n" << usage << std::endl; |
| return -1; |
| } |
| } |
| gflags::ShutDownCommandLineFlags(); |
| return 0; |
| } |