Uh oh!
There was an error while loading. Please reload this page.
Add tsfile-cli: inspect and import .tsfile from the command line - #829
Conversation
Read-only inspect/export verbs (ls/schema/stats/head/cat/select) as a single multi-call `tsfile` binary, backed by the existing C++ reader API.
9 TDD tasks (CMake scaffold -> arg parser -> formatters -> ResultSet pump -> ls/schema/stats/head/cat/select -> install/verify). Spec tweaked to match confirmed C++ APIs (table-model schema blanks encoding/compression; stats = count + time range).
There was a problem hiding this comment.
Pull request overview
Adds a new C++ command-line tool (tsfile-cli) under cpp/tools/ for inspecting/exporting .tsfile contents and importing CSV/TSV into a new table-model .tsfile, with accompanying formatters, command implementations, and a new tool-focused test suite integrated into the existing CMake test target.
Changes:
- Introduces
tsfile-cliexecutable + CLI dispatch/arg parsing, read commands (ls/schema/meta/stats/count/head/cat/sample), and CSV/TSV import (write). - Adds output/input formatting helpers (CSV/TSV/NDJSON/table) and statistics aggregation helpers for metadata-driven commands.
- Integrates tool build + tests into CMake, and adjusts
ReadFile::open()diagnostics to go to stderr.
Reviewed changes
Copilot reviewed 37 out of 38 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
cpp/tools/tools_main.cc | Adds main() entrypoint that forwards argv to run_cli. |
cpp/tools/CMakeLists.txt | Adds tsfile-cli build target and install rule. |
cpp/tools/README.md | Documents build/usage/options and examples for tsfile-cli. |
cpp/tools/skills/tsfile-cli/SKILL.md | Adds a machine-readable skill doc describing CLI usage. |
cpp/tools/cli/cli_args.h | Defines ParsedArgs struct for CLI arguments. |
cpp/tools/cli/cli_args.cc | Implements argument parsing for all commands/options. |
cpp/tools/cli/exit_codes.h | Defines standardized exit codes for CLI. |
cpp/tools/cli/run_cli.h | Declares run_cli entrypoint for CLI execution. |
cpp/tools/cli/run_cli.cc | Implements top-level dispatch, validation, and help/version handling. |
cpp/tools/commands/commands.h | Declares per-command handlers and shared helpers. |
cpp/tools/commands/cmd_ls.cc | Implements ls for devices/tables. |
cpp/tools/commands/cmd_schema.cc | Implements schema listing for tree/table models. |
cpp/tools/commands/cmd_meta.cc | Implements file-level summary output (meta). |
cpp/tools/commands/cmd_stats.cc | Implements per-series statistics output (stats). |
cpp/tools/commands/cmd_count.cc | Implements per-series counts + total (count). |
cpp/tools/commands/cmd_head.cc | Implements head via shared row query helper. |
cpp/tools/commands/cmd_cat.cc | Implements cat via shared row query helper. |
cpp/tools/commands/cmd_sample.cc | Implements reservoir sampling (sample). |
cpp/tools/commands/row_query.cc | Implements shared querying logic used by row-returning commands. |
cpp/tools/commands/cmd_write.cc | Implements CSV/TSV import into new table-model .tsfile (write). |
cpp/tools/commands/stat_table.h | Defines structures/helpers for stats + meta summary extraction. |
cpp/tools/commands/stat_table.cc | Implements metadata/statistics collection used by stats/meta/count. |
cpp/tools/format/output_format.h | Defines output format enum and RowWriter. |
cpp/tools/format/output_format.cc | Implements CSV/TSV/NDJSON/table formatting and escaping. |
cpp/tools/format/input_format.h | Defines column-spec parsing and delimited-line parsing helpers. |
cpp/tools/format/input_format.cc | Implements --columns parsing, CSV quote splitting, bool parsing. |
cpp/tools/format/result_set_format.h | Declares helpers for converting result sets to output. |
cpp/tools/format/result_set_format.cc | Implements result-set streaming and reservoir-sampled output. |
cpp/test/CMakeLists.txt | Wires tool tests into TsFile_Test when BUILD_TOOLS is on; adjusts GTest include handling. |
cpp/test/tools/cli_test_util.h | Adds helpers for CLI fixture generation and temp file naming. |
cpp/test/tools/cli_args_test.cc | Adds unit tests for arg parsing and CLI-level validation. |
cpp/test/tools/input_format_test.cc | Adds unit tests for column spec parsing and CSV/TSV splitting. |
cpp/test/tools/output_format_test.cc | Adds unit tests for escaping, formatting, and table alignment. |
cpp/test/tools/stat_table_test.cc | Adds unit tests for statistic-to-cell conversion logic. |
cpp/test/tools/command_e2e_test.cc | Adds in-process E2E tests including a write→read round-trip. |
cpp/src/file/read_file.cc | Routes ReadFile::open() diagnostics to stderr instead of stdout. |
cpp/CMakeLists.txt | Adds BUILD_TOOLS option and includes cpp/tools subdirectory. |
.gitignore | Ignores local AI tooling dirs and test-run artifacts. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
ColinLeeo
commented
Jun 5, 2026
When handling exceptions in the CLI, we should return readable error messages instead of generic descriptions or error codes. |
- row_query/sample: translate storage error codes to readable phrases instead of emitting a bare numeric code - read_file: drop the always -1 fd_ value from open() diagnostic; keep strerror(errno) - run_cli: honor --help even with a positional file; correct the stats usage text (min/max/first/last/sum) - meta: remove the always-empty version/bloom_filter columns (the public reader API exposes neither); update README and SKILL accordingly - write: stream rows into fixed 1024-row Tablet batches so memory stays bounded regardless of input size - write: reject numeric overflow (ERANGE for int/float/double, plus an INT32 range check) - tools CMake: remove the noisy unconditional configure message - test CMake: drop the no-op SYSTEM target property and force the vendored GTest headers ahead on TsFile_Test where header resolution matters
SpriCoder
commented
Jun 5, 2026
@ColinLeeo Addressed in 7bb6b62. The CLI now translates storage-engine error codes into readable phrases via a |
Resolve cpp/test/CMakeLists.txt conflict: keep develop's GTest acquisition (tar-extract + add_subdirectory instead of FetchContent) and re-apply the AppleClang include-order fix on top, using GTEST_SRC_ROOT. Reformat cmd_meta.cc to satisfy clang-format.
Strict-review follow-up to PR apache#829: - write: reject non-strictly-increasing timestamps per device (tag tuple) with a located message; refuse --output equal to the input file; remove the partial output on any failure so no corrupt .tsfile is left behind - write/query/sample failures now print a human-readable cause via error_code_message() instead of a bare numeric code; the helper lives in the output_format layer so read and write share it - schema: report real encoding/compression for table-model columns instead of always-empty cells - columns spec: reject duplicate column names - reject flags that do not apply to a command (write-only flags on read commands, row/range flags on metadata commands, --header-match with --no-header), and give a clear error when an option precedes the command - rename the read-output helpers to emit_result_set* and the JSON predicate to emits_json_bare so the names match what they do - docs: document the per-device timestamp ordering rule and drop the unimplemented "help <command>" form
The file holds generic statistics helpers (collect_series_stats, collect_file_summary, statistic_value_cells) used by stats/count/meta for both the tree and table models. "table" wrongly implied the table model; "statistics" describes what it actually provides.
Covers per-device timestamp-order rejection (including across batch flushes), --output anti-alias and unlink-on-failure, large streaming round-trip, numeric overflow detection, duplicate-column rejection, flag-applicability errors, the leading-option error, error_code_message mapping, --help with a positional file, and table-model schema encoding/compression.
Derive the scoped flag from is_row instead of re-listing the head/cat/sample command names, so the row-command set lives in one place.
| TSFILE_CLI_VERSION="${TsFile_CPP_VERSION}") | ||
| add_executable(tsfile_cli tools_main.cc $<TARGET_OBJECTS:tsfile_cli_obj>) | ||
| target_include_directories(tsfile_cli PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) |
There was a problem hiding this comment.
include_directories(
${LIBRARY_INCLUDE_DIR}
${THIRD_PARTY_INCLUDE}
)
do not use ${CMAKE_CURRENT_SOURCE_DIR} pls
There was a problem hiding this comment.
Addressed in a8c38e3. tsfile_cli_obj now includes library headers from the staged ${LIBRARY_INCLUDE_DIR} + ${THIRD_PARTY_INCLUDE} instead of reaching into ${PROJECT_SOURCE_DIR}/src; ${CMAKE_CURRENT_SOURCE_DIR} stays only for the tool's own cli/ commands/ format/ headers. This matches what test/CMakeLists.txt already does (include_directories(${LIBRARY_INCLUDE_DIR} ${THIRD_PARTY_INCLUDE}) for the library + -I .../tools for tool headers). Verified the Debug build still resolves all headers from the staged include tree.
There was a problem hiding this comment.
remove CMAKE_CURRENT_SOURCE_DIR
There was a problem hiding this comment.
Done in #835 — both ${CMAKE_CURRENT_SOURCE_DIR} usages are replaced with ${CMAKE_SOURCE_DIR}/tools (for tsfile_cli_obj and the tsfile_cli executable), matching the convention already used in cpp/test/CMakeLists.txt. Note the executable line is not redundant: $<TARGET_OBJECTS:tsfile_cli_obj> only pulls in object files and does not propagate the object library's PUBLIC include dirs, so tools_main.cc still needs tools/ on its include path. Verified the Debug build links tsfile_cli and resolves all headers from the staged include tree.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
ColinLeeo
commented
Jun 7, 2026
Good Job! |
- write: support quoted CSV fields that span multiple physical lines by
joining continuation lines until the opening double-quote closes, via a
new read_record() helper shared by the header and data read paths
- read: print FLOAT/DOUBLE with max_digits10 precision so values
round-trip losslessly through a pipe instead of the default 6 sig digits
- tools CMake: include library headers from the staged LIBRARY_INCLUDE_DIR
and THIRD_PARTY_INCLUDE instead of reaching into ${PROJECT_SOURCE_DIR}/src,
matching the convention the test target already uses
- tests: cover read_record (multi-line quoted, CR strip, TSV, unterminated
quote) and add e2e float/double lossless and embedded-newline import casesUh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Implements jt2594838's review on develop's tsfile-cli:
- write: accept TIMESTAMP/DATE/BLOB columns and store each column with the
engine's default encoding/compression for its type (via get_value_encoder /
get_default_compressor) instead of hard-coded PLAIN/UNCOMPRESSED.
- sample/row_query: when no device is given, build query paths from one
whole-file get_timeseries_metadata() call via a shared collect_tree_query_paths
helper instead of a per-device schema loop.
- Sink CSV split/escape into common/csv_utils.{h,cc} and the datatype-name parser
into common::parse_data_type_name; the CLI now delegates to them.
- write: case-insensitive column category, verbose echo of the resolved config,
header-match errors that name the offending column/position, error codes
appended to writer failures, and result_code naming.
- Documented the table-model "first table only" caveat and the extended write
types in README/SKILL; added struct comments and clearer usage text.
- Tests: extended-type/category parsing, TIMESTAMP/DATE/BLOB round-trip, verbose
echo, header-match position, and common csv/type-name helpers.Error strings for flags that have a short form now mention it, matching the usage text: -t/--table, -d/--device, -n/--limit. Flags without a short alias (--columns, --offset, --seed, --start/--end, --header-match) stay long-only.
Add a comment explaining that BOOLEAN is grouped with the numeric types because true/false are bare JSON literals (not because BOOLEAN is numeric); strings, blobs, and dates are quoted. The misleadingly-named is_numeric was already renamed to emits_json_bare in a prior round.
- Move (not copy) each fully-built DataRow into the batch vector; the row owns a vector<string> of cells, so the lvalue push_back copied every cell per row. - Add values to the Tablet by column index instead of by name. The by-name add_value lowercases the measurement name and does a map lookup per cell; the column->index order is fixed at schema-build time, so the index is known. String/text/blob use the c-string by-index overload to keep the same path.
New e2e tests: - per-column value mapping via JSON output (guards the by-index add_value path: any cross-column write would surface as a wrong key/value pair) - multi-type import across >2 batches, spot-checking a last-batch row - RFC 4180 round-trip of a quoted STRING with comma + embedded quotes - TIMESTAMP overflow / non-numeric, empty-cell-as-null - DATE boundary (leap day) and impossible-date rejection The impossible-date test exposed a bug: parse_date_cell only checked the YYYY-MM-DD shape, and the writer silently drops an invalid std::tm rather than erroring, so e.g. 2024-13-40 imported as success. parse_date_cell now validates via DateConverter::date_to_int and reports "bad DATE".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Resolve the device's real series and build query paths directly from them, keeping only measurements that match the projection. A provided measurement that doesn't exist on the device is now dropped instead of queried blindly, matching the no-device path.
Expresses the existence test directly instead of counting; equivalent for a std::set and C++11-compatible (no contains()).
Uh oh!
There was an error while loading. Please reload this page.
Summary
Adds
tsfile-cli, a single pipe-friendly C++ command-line tool (cpp/tools/) for working with.tsfilefiles from the shell. Built entirely on the existingstorage::TsFileReader/TsFileTableWriterAPIs; no storage-engine changes.Inspect / export (read-only)
ls— list devices (tree model) or tables (table model)schema— per-series datatype / encoding / compressionmeta— file-level summary: model, counts, global time range, file sizestats— per-seriescount, start, end, min, max, first, last, sum(from statistics, no page scan)count— per-series row counts + totalhead/cat— preview / stream rows, with projection (-m), time range (--start/--end),--offset/-nsample— deterministic reservoir sample (--seed)csv|tsv|json|table(TTY-adaptive); data → stdout, diagnostics → stderr; exit codes0/1/2/3Import (write)
write— import CSV/TSV rows into a new table-model.tsfile--columns name:TYPE:tag|fieldschema (no type inference); first input column is the timestamp-);-ooutput;-f csv|tsv; optional--no-header/--header-match-vprints a one-line summary), Unix-styleOther
ReadFile::openerrors now go to stderr (were stdout) so read output stays pipe-cleanTest plan
cd cpp && bash build.sh -t=Debugbuildsbin/tsfile-cli+TsFile_Test(add--disable-antlr4on CMake >= 4)InputFormatTest,ParseArgsTest,RunCliTest,CliE2E,RowWriterTest,StatTableTesttsfile-cli meta|ls|schema|stats|count <file.tsfile>printf 'time,id,v\n0,d,1\n' | tsfile-cli write --table t --columns "id:STRING:tag,v:INT64:field" -o out.tsfile -thentsfile-cli count -f tsv out.tsfile