This is an automated email from the ASF dual-hosted git repository.
gavinchou pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 13735f9f797 [improvement](be) Add complete segment validation and raw
CRC checks to meta tool (#67183)
13735f9f797 is described below
commit 13735f9f7973b041dca2072c00337456202944f0
Author: Gavin Chou <[email protected]>
AuthorDate: Thu Aug 27 15:57:46 2026 +0800
[improvement](be) Add complete segment validation and raw CRC checks to
meta tool (#67183)
Problem Summary:
`meta_tool --operation=show_segment_data` previously read a fixed 10-row
sample, disabled page checksum verification, retained all returned
values in one batch, and did not reliably propagate read failures
through its process exit code. It could therefore reproduce a bad page,
but could not prove that every logical row in a repaired segment was
readable.
Metadata-driven reads are also insufficient for forensic analysis when
the segment footer or ordinal indexes contain stale physical offsets. A
valid page may still exist at a corrected physical offset, but the
normal reader will continue to use the stored `PagePointer`.
This PR:
- adds bounded row controls to `show_segment_data`:
- `--rows` (`-1` reads all remaining rows)
- `--row_start`
- `--batch_rows`
- `--check_only`
- `--verify_checksum`
- `--scan_segment_pages`
- makes segment inspection return meaningful status and exit codes:
- `0`: success
- `1`: corruption, I/O, checksum, decoding, or boundary failure
- `2`: invalid command-line input
- adds `--operation=check_page_crc` for explicit physical `OFFSET:SIZE`
ranges
- adds `--operation=scan_page_crc` for deriving a consecutive page chain
between a known start and an exact expected end
- validates CRC32C, page footer size, and `PageFooterPB` with bounded
one-MiB reads
- documents operation selection, output modes, limitations, and a
practical inserted-extent investigation workflow
The raw CRC operations are read-only forensic checks. They do not modify
a segment, discover an arbitrary first-page offset, prove that a page is
referenced by metadata, or replace full logical decoding.
### Release note
Add bounded full-segment validation and raw physical page CRC operations
to the BE `meta_tool`.
### Check List (For Author)
- Test
- [ ] Regression test
- [ ] Unit Test
- [x] Manual test (add detailed scripts or steps below)
- [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
- [ ] Previous test can cover this change.
- [ ] No code files have been changed.
- [ ] Other reason
Manual validation:
- `sh build.sh --be --meta-tool -j 32`
- `sh format_code.sh be/src/tools/meta_tool.cpp`
- `build-support/check-format.sh` with clang-format 16
- `git diff --check`
- decoded all 43 columns and 1,211,072 rows per column with `--rows=-1
--check_only=true --verify_checksum=true --scan_segment_pages=true`
- validated three representative explicit page ranges, including data
and index pages
- scanned and validated a continuous 815-page range, ending exactly at
the requested end offset
- confirmed that the known corrupt extent returns corruption and exit
code 1
- checked invalid range syntax, conflicting range inputs, missing scan
bounds, invalid output mode, and out-of-file ranges
`run-clang-tidy.sh` was also attempted. The new output-parameter
findings were fixed, but the command remains blocked by
existing/toolchain issues including an unmatched `NOLINTEND` in
`be/src/core/types.h` and the clang-20 toolchain failing to resolve
`stddef.h`.
- Does this need documentation?
- [ ] No.
- [x] Yes. `be/src/tools/README.md` is added and expanded in this PR.
---
be/src/tools/README.md | 439 +++++++++++++++++++++++
be/src/tools/meta_tool.cpp | 860 ++++++++++++++++++++++++++++++++++++++++-----
2 files changed, 1210 insertions(+), 89 deletions(-)
diff --git a/be/src/tools/README.md b/be/src/tools/README.md
new file mode 100644
index 00000000000..7e919b94e1e
--- /dev/null
+++ b/be/src/tools/README.md
@@ -0,0 +1,439 @@
+# Doris BE Meta Tool
+
+`meta_tool` inspects and maintains Doris BE metadata. This document focuses on
the read-only
+segment inspection operations used for diagnosing and validating segment files.
+
+## Build
+
+Build the BE meta tool from the Doris repository root:
+
+```bash
+sh build.sh --be --meta-tool -j 32
+```
+
+The installed binary is normally available at:
+
+```text
+be/output/lib/meta_tool
+```
+
+If the loader reports `libjvm.so: cannot open shared object file`, add the JDK
server library before
+running the examples:
+
+```bash
+export LD_LIBRARY_PATH="${JAVA_HOME}/lib/server:${LD_LIBRARY_PATH:-}"
+```
+
+## Inspect a segment footer
+
+```bash
+be/output/lib/meta_tool \
+ --operation=show_segment_footer \
+ --file=/path/to/segment.dat
+```
+
+This parses and prints `SegmentFooterPB`. It does not decode column values.
+
+## Check physical page CRCs
+
+Doris segment pages use this physical trailer layout:
+
+```text
+page body | PageFooterPB | footer_size (4-byte little endian) | crc32c (4-byte
little endian)
+```
+
+The stored CRC covers every byte except the final four-byte CRC itself. The
page CRC operations
+also validate that `footer_size` stays inside the supplied page range and that
the footer parses as
+a `PageFooterPB` with its required logical fields.
+
+There are three related but different page checks:
+
+| Check | Page locations come from | Intended use |
+|---|---|---|
+| `show_segment_data --scan_segment_pages=true` | Segment footer and ordinal
indexes | Validate metadata-referenced data and dictionary pages while reading
the segment normally. |
+| `--operation=check_page_crc` | Caller-supplied `offset:size` values | Test
known or corrected physical `PagePointer` candidates without trusting segment
metadata. |
+| `--operation=scan_page_crc` | A caller-supplied start and end offset |
Derive consecutive page sizes from CRC trailers when the first page boundary is
already known. |
+
+### When to use each operation
+
+Start with the normal metadata-driven path whenever the segment footer and
indexes are expected to
+be trustworthy. The raw CRC operations are forensic tools for the cases where
normal reading has
+already failed or where a candidate physical layout must be tested
independently of the offsets
+stored in the segment.
+
+| Situation | Recommended operation | Reason |
+|---|---|---|
+| Validate that a healthy or repaired segment can really serve all logical
rows | `show_segment_data --rows=-1 --check_only=true --verify_checksum=true` |
Exercises normal footer, index, iterator, decompression, decoding, and checksum
paths. |
+| Inventory metadata-referenced data and dictionary pages | Add
`--scan_segment_pages=true` to `show_segment_data` | Uses the same PagePointers
that Doris will use in production and reports page-level layout/CRC results. |
+| Check one or more page locations whose exact physical offsets and sizes are
already known | `check_page_crc` | Directly tests each supplied `offset:size`
range without following the possibly wrong PagePointer stored in metadata. |
+| Test whether adding a suspected constant delta to metadata offsets recovers
pages | `check_page_crc` | The corrected offset and the original metadata size
form an exact candidate range. Sampling pages near the start, middle, and end
tests whether one displacement explains the suffix. |
+| Validate a candidate first-page boundary when individual page sizes are
unknown | `scan_page_crc` | Derives each page end from CRC/footer evidence and
requires a gap-free chain to the supplied end. |
+| Search an entire file for an unknown first-page offset | Neither raw
operation directly supports this | `scan_page_crc` verifies a caller-provided
start; it does not enumerate possible starts. First derive candidates from
metadata, an S3 part boundary, byte comparison, logs, or another independent
source. |
+| Determine whether bytes are semantically correct merely because their CRC
matches | Neither raw operation is sufficient | CRC proves physical consistency
of a selected page range, not business-value correctness. Follow with normal
full decoding. |
+| Modify or repair the segment | Neither operation | Both operations are
read-only. Repair must be performed separately and then validated through the
normal read path. |
+
+The distinction matters because Doris page reads are address based, not
self-synchronizing. The
+reader obtains an absolute `PagePointer(offset, size)` from the footer or an
index and reads exactly
+that byte range. It does not search forward for another valid page after a
checksum mismatch. If an
+unaccounted extent is inserted in the middle of the file, the footer at the
physical EOF can still
+parse successfully while every PagePointer after the insertion addresses the
wrong bytes:
+
+```text
+metadata/logical layout
+... previous page | next page at logical offset L | following pages ...
+ ^ PagePointer offset L
+
+damaged physical layout
+... previous page | unaccounted bytes | next page at physical offset P |
following pages ...
+ ^ reader still uses L ^ candidate P = L + delta
+```
+
+This is the main use case for the two raw operations. `scan_page_crc` can test
whether `P` begins a
+continuous page chain, while `check_page_crc` can test representative metadata
ranges after adding
+the same `delta` to their offsets.
+
+### Practical investigation workflow: inserted extent at an S3 boundary
+
+The following workflow comes from a real `Bad page: checksum mismatch`
investigation. The segment
+footer was readable, pages before logical offset `5,203,052` were readable,
and pages after that
+offset failed. Metadata independently placed both the end of the
key/primary-index group and the
+first value-column page at logical offset `5,203,052`.
+
+The configured S3 write-buffer boundary was 5 MiB:
+
+```text
+5 MiB = 5,242,880
+candidate delta = 5,242,880 - 5,203,052 = 39,828
+```
+
+The 5 MiB value supplied a candidate physical start; the CRC scanner did not
discover that start.
+Use the following sequence when investigating a similar layout.
+
+1. Preserve the original file and record its length and checksum. Run
`show_segment_footer` and a
+ normal `show_segment_data` check first. Record the first failing
column/page and whether all
+ metadata-referenced pages after some logical boundary fail.
+
+2. Establish the logical boundary `L` from metadata. Useful independent
signals include the end of
+ the last readable PagePointer, the start of the first failing data page,
vertical-compaction
+ key/value group boundaries, and the expected logical EOF derived from the
footer.
+
+3. Propose one or more physical starts `P` from evidence outside the CRC
scanner. Examples include
+ S3 multipart boundaries, file-cache block boundaries, direct object-range
comparison, known
+ alignment, or a constant difference between the actual and expected file
sizes.
+
+4. Check whether `[L,P)` itself forms a page chain. For the example, scanning
the suspected extra
+ extent is expected to fail:
+
+ ```bash
+ be/output/lib/meta_tool \
+ --operation=scan_page_crc \
+ --file=/path/to/bad-segment.dat \
+ --page_scan_start=5203052 \
+ --page_scan_end=5242880 \
+ --page_output=errors
+ ```
+
+ The observed result was `no_page_boundary offset=5203052 remaining=39828`
and exit code 1. This
+ does not by itself prove that the bytes are corrupt; it says only that the
suspected extent is
+ not a consecutive Doris page chain beginning at `L`.
+
+5. Scan forward from candidate `P` to an independently expected end. In the
example, the following
+ range was expected to contain all 91 pages of column 1:
+
+ ```bash
+ be/output/lib/meta_tool \
+ --operation=scan_page_crc \
+ --file=/path/to/bad-segment.dat \
+ --page_scan_start=5242880 \
+ --page_scan_end=5919643 \
+ --page_output=all
+ ```
+
+ It produced 91 consecutive CRC- and footer-valid pages, with no gaps or
overlaps, and ended
+ exactly at `5,919,643`. The page count and end position also matched the
metadata after adding
+ `39,828`. A long exact chain is much stronger boundary evidence than one
isolated CRC match.
+
+6. Use `check_page_crc` to test the same delta at widely separated locations.
Keep the sizes from
+ metadata and add `39,828` only to offsets:
+
+ ```bash
+ be/output/lib/meta_tool \
+ --operation=check_page_crc \
+ --file=/path/to/bad-segment.dat \
+ --page_ranges=5242880:6485,5919754:47,264635689:1384,274697492:2932 \
+ --page_output=all
+ ```
+
+ These candidates cover the first displaced data page, another small data
page, a middle/late
+ ordinal root, and a page near the file tail. If all pass while their
uncorrected metadata offsets
+ fail, one inserted extent and a constant suffix displacement is a better
explanation than many
+ unrelated page corruptions.
+
+7. If creating a non-destructive forensic copy by removing the suspected
extent, preserve the
+ original and verify byte identity on both sides of the edit. Then validate
the copy through the
+ normal path, not only with raw CRCs:
+
+ ```bash
+ be/output/lib/meta_tool \
+ --operation=show_segment_data \
+ --file=/path/to/repaired-copy.dat \
+ --rows=-1 \
+ --batch_rows=4096 \
+ --check_only=true \
+ --verify_checksum=true \
+ --scan_segment_pages=true
+ ```
+
+ In the incident, deleting only `[5,203,052,5,242,880)` made the suffix
byte-for-byte identical
+ to the expected logical layout; 9,060 data pages and six dictionary pages
passed, and all 11
+ columns were readable. That establishes the repaired copy as forensic
evidence, but does not by
+ itself authorize replacing a production object or its rowset metadata.
+
+### Interpreting raw CRC results
+
+| Observation | What it supports | What it does not prove |
+|---|---|---|
+| One explicit range has matching CRC and a valid footer | The supplied offset
and size delimit an internally consistent Doris page. | The page is referenced
by this segment, contains the intended values, or is at the correct logical
position. |
+| Corrected range passes while the metadata range of the same size fails | The
candidate physical displacement is plausible for that page. | A single page is
not enough to prove one constant displacement for the whole suffix. |
+| Many corrected ranges near the start, middle, and tail pass with the same
delta | A single inserted/omitted physical extent is a strong explanation. |
Whether the discrepancy originated in object upload, object storage, cache,
warm-up, range download, or local copying. |
+| `scan_page_crc` finds a long continuous chain and ends exactly at an
independently expected end | The proposed start is strongly supported as a real
page boundary. | How the start was created or that it would have been found
without the supplied candidate. |
+| `no_page_boundary` occurs at the first offset | No valid page chain was
found beginning exactly there within the supplied range. | The range contains
no valid page at some later, unknown start. |
+| A chain stops after several valid pages | The valid prefix is physically
consistent; the next page is corrupt, the requested end is wrong, or non-page
bytes follow. | Which of those causes applies without more metadata or
byte-layout evidence. |
+| Raw page checks pass but normal full decoding fails | Physical page framing
is intact for the checked ranges. | Compression, encoding, index linkage,
schema, logical ordering, or value semantics may still be wrong. |
+
+For an unknown start, avoid running `scan_page_crc` across a multi-gigabyte
range and treating its
+failure as discovery. The implementation performs a byte-by-byte running CRC
search from exactly
+the supplied start, so the work is proportional to the distance to the next
accepted boundary.
+Generate a small set of defensible candidate starts first, then test each one.
+
+Raw CRC operation flags:
+
+| Flag | Default | Operation | Description |
+|---|---:|---|---|
+| `--file` | empty | both | Local segment path; required. |
+| `--page_ranges` | empty | `check_page_crc` | Comma-separated decimal
`OFFSET:SIZE` values. |
+| `--page_ranges_file` | empty | `check_page_crc` | File containing decimal
`OFFSET SIZE` pairs. |
+| `--page_scan_start` | `0` | `scan_page_crc` | Inclusive known first-page
offset; the flag must be explicitly supplied even when the value is zero. |
+| `--page_scan_end` | `0` | `scan_page_crc` | Exclusive expected end offset;
the flag must be explicitly supplied. |
+| `--page_output` | `all` | both | Per-page output level: `all`, `errors`, or
`summary`. |
+
+### Check known page ranges
+
+Use `--page_ranges` for a small number of decimal `OFFSET:SIZE` pairs:
+
+```bash
+be/output/lib/meta_tool \
+ --operation=check_page_crc \
+ --file=/path/to/segment.dat \
+ --page_ranges=5242880:18349,32162899:2387
+```
+
+Whitespace around comma-separated ranges is accepted, but hexadecimal input is
not. Ranges are
+checked in their input order and duplicates are preserved. Every range is
checked even if an
+earlier range fails; the final exit status is non-zero when any range is
invalid, unreadable, has a
+bad CRC, or contains an invalid footer.
+
+For a large list, use `--page_ranges_file` instead of `--page_ranges`:
+
+```bash
+be/output/lib/meta_tool \
+ --operation=check_page_crc \
+ --file=/path/to/segment.dat \
+ --page_ranges_file=/path/to/page_ranges.txt
+```
+
+The ranges file contains one decimal `OFFSET SIZE` pair per line. Blank lines,
leading/trailing
+whitespace, full-line comments, and trailing comments are accepted:
+
+```text
+# column 1 data page 0
+5242880 18349
+
+32162899 2387 # column 1 ordinal root
+```
+
+Exactly one of `--page_ranges` and `--page_ranges_file` is required.
+
+### Scan consecutive pages from a known boundary
+
+Use `scan_page_crc` when the first candidate page offset and the expected end
of a continuous page
+range are known, but individual page sizes are not:
+
+```bash
+be/output/lib/meta_tool \
+ --operation=scan_page_crc \
+ --file=/path/to/segment.dat \
+ --page_scan_start=5242880 \
+ --page_scan_end=32203820
+```
+
+The range is `[page_scan_start, page_scan_end)`. Starting at
`page_scan_start`, the scanner extends
+CRC32C one byte at a time until the following four bytes match the running
CRC, then validates the
+candidate page footer. A successful page becomes the start of the next search.
Success requires at
+least one page and requires the final page to end exactly at `page_scan_end`.
+
+`scan_page_crc` does **not** search for an arbitrary first page offset. If
`page_scan_start` points
+to padding, a corrupt extent, or the middle of a page, it reports
`no_page_boundary`. Candidate
+physical starts such as an S3 multipart boundary must be proposed from
independent evidence before
+using this operation.
+
+### Page output control
+
+Both raw CRC operations accept `--page_output`:
+
+| Value | Output |
+|---|---|
+| `all` | Print every checked or discovered page, followed by the summary.
This is the default. |
+| `errors` | Print only failed page records and `no_page_boundary`, followed
by the summary. |
+| `summary` | Suppress individual page records and print only headers and the
final summary. |
+
+A successful page record looks like:
+
+```text
+page=0 offset=5242880 size=18349 range_valid=true readable=true
actual=929431102 expect=929431102 checksum_ok=true footer_size=15
footer_ok=true page_type=DATA_PAGE
+```
+
+The final result is designed for shell automation:
+
+```text
+=== Page CRC Summary ===
+Mode: ranges
+Pages Checked: 2
+Valid: 2
+Bad CRC: 0
+Bad Footer: 0
+Unreadable: 0
+Invalid Range: 0
+Status: OK
+```
+
+The implementation uses bounded one-MiB reads for CRC calculation and
scanning. It does not load
+the complete segment or complete scan range into memory.
+
+## Dump or check segment data
+
+The default command prints the first 10 logical rows from every top-level
scalar column:
+
+```bash
+be/output/lib/meta_tool \
+ --operation=show_segment_data \
+ --file=/path/to/segment.dat
+```
+
+### Row-reading flags
+
+| Flag | Default | Description |
+|---|---:|---|
+| `--rows` | `10` | Maximum logical rows read per column. Use `-1` for all
remaining rows and `0` to read no values. |
+| `--row_start` | `0` | First logical row ordinal to read. |
+| `--batch_rows` | `4096` | Maximum rows decoded by each `next_batch` call.
This bounds working memory and does not change the selected row range. |
+| `--check_only` | `false` | Decode selected rows without printing every
value. A compact result is printed for each column. |
+| `--verify_checksum` | `true` | Verify page checksums through the normal
`ColumnReader`/`PageIO` path. |
+| `--scan_segment_pages` | `false` | Independently scan data pages referenced
by ordinal indexes and dictionary pages, and print CRC/layout summaries. |
+
+`--rows` applies to each selected top-level column. Doris segments are
columnar, so dumping all
+rows from a 43-column segment prints approximately 43 values per logical row,
not 1 output line per
+row.
+
+If `--rows` exceeds the number of rows remaining after `--row_start`, the
range is clamped at the
+segment row count. `--row_start` greater than the segment row count and
`--batch_rows=0` are invalid.
+
+### Examples
+
+Print the first 1,000 rows from every scalar column:
+
+```bash
+be/output/lib/meta_tool \
+ --operation=show_segment_data \
+ --file=/path/to/segment.dat \
+ --rows=1000
+```
+
+Print logical rows `[100000, 101000)`:
+
+```bash
+be/output/lib/meta_tool \
+ --operation=show_segment_data \
+ --file=/path/to/segment.dat \
+ --row_start=100000 \
+ --rows=1000
+```
+
+Dump every row. Redirecting stdout is recommended for large segments:
+
+```bash
+be/output/lib/meta_tool \
+ --operation=show_segment_data \
+ --file=/path/to/segment.dat \
+ --rows=-1 \
+ --verify_checksum=true \
+ > segment.dump.txt
+```
+
+Decode every row without printing values:
+
+```bash
+be/output/lib/meta_tool \
+ --operation=show_segment_data \
+ --file=/path/to/segment.dat \
+ --rows=-1 \
+ --batch_rows=4096 \
+ --check_only=true \
+ --verify_checksum=true
+```
+
+For a stronger forensic check, combine full decoding with the independent page
scan:
+
+```bash
+be/output/lib/meta_tool \
+ --operation=show_segment_data \
+ --file=/path/to/segment.dat \
+ --rows=-1 \
+ --batch_rows=4096 \
+ --check_only=true \
+ --verify_checksum=true \
+ --scan_segment_pages=true
+```
+
+Successful full decoding ends with a summary similar to:
+
+```text
+=== Data Read Summary ===
+Columns Checked: 43
+Rows Per Column: 1211072
+Row Range: [0,1211072)
+Check Only: true
+Verify Checksum: true
+Status: OK
+```
+
+## Validation scope and limitations
+
+Full `check_only` decoding verifies that every selected logical row can be
read through the column
+iterator, including page checksum verification when `--verify_checksum=true`.
It exercises data,
+dictionary, encoding, compression, nullable-column, and ordinal-index read
paths needed for the
+selected columns.
+
+`--scan_segment_pages=true` independently checks every data page exposed by
each ordinal index and
+each dictionary page. It does not currently enumerate every auxiliary
zone-map, Bloom-filter, or
+bitmap-index page, so success must not be described as exhaustive validation
of all auxiliary
+indexes.
+
+`check_page_crc` and `scan_page_crc` operate on raw physical ranges and do not
prove that those
+pages are referenced by the segment footer or rowset metadata. A single CRC32C
match is not by
+itself proof that an arbitrary offset is a page boundary; footer parsing,
exact range completion,
+multiple consecutive pages, and agreement with independent metadata materially
strengthen the
+evidence. Neither operation modifies or repairs its input file.
+
+Top-level complex columns are not yet supported by `check_only`; the command
returns a non-zero
+status instead of silently reporting success. Displayed string-like values are
escaped and truncated
+after 50 bytes, so `show_segment_data` is a readability/debug dump rather than
a round-trippable
+logical export.
+
+## Exit status
+
+| Code | Meaning |
+|---:|---|
+| `0` | All requested rows or pages completed successfully; CRC scanning ended
exactly at its requested end. |
+| `1` | The file/ranges file could not be read, or a footer, checksum, page,
iterator, decoding, boundary, or early-EOF error occurred. |
+| `2` | Required flags, row/page options, range syntax, output mode, or scan
bounds are invalid. |
diff --git a/be/src/tools/meta_tool.cpp b/be/src/tools/meta_tool.cpp
index 99970f8a938..bdf060164e0 100644
--- a/be/src/tools/meta_tool.cpp
+++ b/be/src/tools/meta_tool.cpp
@@ -21,14 +21,18 @@
#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"
@@ -82,7 +86,8 @@ 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, gen_empty_segment");
+ "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");
@@ -94,6 +99,22 @@ DEFINE_string(output_path, "", "output directory path
(default: current director
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;
@@ -110,7 +131,15 @@ std::string get_usage(const std::string& progname) {
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\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";
@@ -572,23 +601,589 @@ std::string format_column_value(const doris::IColumn&
column, size_t row,
}
}
-// Read and print column data values
-void print_column_data_values(const doris::segment_v2::ColumnMetaPB&
column_meta,
- const FileReaderSPtr& file_reader, uint64_t
num_segment_rows,
- int indent_level) {
+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;
+ }
- doris::FieldType field_type =
static_cast<doris::FieldType>(column_meta.type());
+ 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;
- // Skip complex types for now
- if (!doris::is_scalar_type(field_type)) {
- std::cout << indent << "(Complex type - cannot display values)" <<
std::endl;
- return;
+ 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 (num_segment_rows == 0) {
- std::cout << indent << "(No data)" << std::endl;
- return;
+ 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
@@ -602,24 +1197,24 @@ void print_column_data_values(const
doris::segment_v2::ColumnMetaPB& column_meta
// Create column reader
ColumnReaderOptions reader_opts;
- reader_opts.verify_checksum = false; // Don't verify checksum for
performance
+ 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()) {
- std::cout << indent << "(Failed to create column reader: " <<
status.to_string() << ")"
- << std::endl;
- return;
+ 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()) {
- std::cout << indent << "(Failed to create column iterator: " <<
status.to_string() << ")"
- << std::endl;
- return;
+ status.prepend("failed to create iterator for column " +
+ std::to_string(column_meta.column_id()) + ": ");
+ return status;
}
// Initialize iterator
@@ -630,77 +1225,116 @@ void print_column_data_values(const
doris::segment_v2::ColumnMetaPB& column_meta
status = iterator->init(iter_opts);
if (!status.ok()) {
- std::cout << indent << "(Failed to initialize column iterator: " <<
status.to_string()
- << ")" << std::endl;
- return;
+ 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();
}
- // Seek to the beginning
- status = iterator->seek_to_ordinal(0);
+ status = iterator->seek_to_ordinal(row_start);
if (!status.ok()) {
- std::cout << indent << "(Failed to seek to ordinal 0: " <<
status.to_string() << ")"
- << std::endl;
- return;
+ status.prepend("failed to seek column " +
std::to_string(column_meta.column_id()) +
+ " to row " + std::to_string(row_start) + ": ");
+ return status;
}
- // Create destination column for reading data
auto data_type =
doris::DataTypeFactory::instance().create_data_type(column_meta);
if (!data_type) {
- std::cout << indent << "(Failed to create data type for field type "
- << static_cast<int>(field_type) << ")" << std::endl;
- return;
+ return Status::InternalError("failed to create data type for column
{}, field type {}",
+ column_meta.column_id(),
static_cast<int>(field_type));
}
- doris::MutableColumnPtr dst_column = data_type->create_column();
-
- // Determine how many rows to display (max 10 rows for readability)
- const size_t max_display_rows = 10;
- size_t rows_to_read = std::min(static_cast<size_t>(num_segment_rows),
max_display_rows);
- size_t rows_read = rows_to_read;
-
- status = iterator->next_batch(&rows_read, dst_column);
- if (!status.ok()) {
- std::cout << indent << "(Failed to read column data: " <<
status.to_string() << ")"
- << std::endl;
- return;
+ if (!FLAGS_check_only) {
+ std::cout << indent << "Data Values (rows [" << row_start << ","
+ << (row_start + rows_to_read) << ") of " << num_segment_rows
<< "):" << std::endl;
}
- if (rows_read == 0) {
- std::cout << indent << "(No data read)" << std::endl;
- return;
- }
+ 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());
+ }
- // Print the values
- std::cout << indent << "Data Values (" << rows_read << " of " <<
num_segment_rows
- << " rows, showing first " << std::min(rows_read,
max_display_rows)
- << "):" << std::endl;
-
- for (size_t i = 0; i < rows_read; ++i) {
- std::cout << indent << " [" << 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);
+ 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;
}
- } else {
- std::cout << format_column_value(*dst_column, i, field_type);
}
- std::cout << std::endl;
+
+ decoded_rows += rows_read;
+ ++batches;
}
- if (num_segment_rows > max_display_rows) {
- std::cout << indent << " ... (" << (num_segment_rows -
max_display_rows) << " more rows)"
- << std::endl;
+ 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
-void print_column_meta(const doris::segment_v2::ColumnMetaPB& column_meta,
- const FileReaderSPtr& file_reader, uint64_t
num_segment_rows,
- int indent_level) {
+// 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()) {
@@ -709,7 +1343,7 @@ void print_column_meta(const
doris::segment_v2::ColumnMetaPB& column_meta,
column_name = "column_id_" + std::to_string(column_meta.column_id());
}
- doris::FieldType field_type =
static_cast<doris::FieldType>(column_meta.type());
+ 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())
@@ -740,7 +1374,9 @@ void print_column_meta(const
doris::segment_v2::ColumnMetaPB& column_meta,
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 << ", ";
+ if (i > 0) {
+ std::cout << ", ";
+ }
const auto& index_meta = column_meta.indexes(i);
if (index_meta.has_type()) {
switch (index_meta.type()) {
@@ -767,19 +1403,27 @@ void print_column_meta(const
doris::segment_v2::ColumnMetaPB& column_meta,
// 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) {
- print_column_meta(column_meta.children_columns(i), file_reader,
num_segment_rows,
- indent_level + 1);
+ 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;
+ return Status::OK();
}
// Print column data values for scalar types
if (doris::is_scalar_type(field_type)) {
- print_column_data_values(column_meta, file_reader, num_segment_rows,
indent_level);
+ return print_column_data_values(column_meta, file_reader,
num_segment_rows, row_start,
+ rows_to_read, indent_level);
} else {
- std::cout << indent << "(Complex type - cannot display values)" <<
std::endl;
+ return Status::NotSupported("cannot display values for column {} with
type {}",
+ column_meta.column_id(),
get_field_type_string(field_type));
}
}
@@ -791,7 +1435,9 @@ ACCESS_PRIVATE_FIELD(ExecEnv_orphan_mem_tracker,
doris::ExecEnv,
ACCESS_PRIVATE_STATIC_FIELD(ExecEnv_tracking_memory, doris::ExecEnv,
std::atomic_bool,
_s_tracking_memory);
-void show_segment_data(const std::string& file_name) {
+// 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();
@@ -815,23 +1461,30 @@ void show_segment_data(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;
+ status.prepend("failed to open segment file " + file_name + ": ");
+ return status;
}
SegmentFooterPB footer;
status = get_segment_footer(file_reader.get(), &footer);
if (!status.ok()) {
- std::cout << "get footer failed: " << status.to_string() << std::endl;
- return;
+ 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;
}
@@ -849,7 +1502,8 @@ void show_segment_data(const std::string& file_name) {
// Print each column
for (int i = 0; i < footer.columns_size(); ++i) {
const auto& column_meta = footer.columns(i);
- print_column_meta(column_meta, file_reader, footer.num_rows(), 0);
+ 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()) {
@@ -918,6 +1572,16 @@ void show_segment_data(const std::string& file_name) {
<< 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() {
@@ -1089,11 +1753,29 @@ int main(int argc, char** argv) {
show_segment_footer(FLAGS_file);
} else if (FLAGS_operation == "show_segment_data") {
if (FLAGS_file == "") {
- std::cout << "no file flag for show_segment_data" << std::endl;
- return -1;
+ 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();
- show_segment_data(FLAGS_file);
+ 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 {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]