This is an automated email from the ASF dual-hosted git repository.
jt2594838 pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/tsfile.git
The following commit(s) were added to refs/heads/develop by this push:
new b4646b62a fix invalid datatype. (#864)
b4646b62a is described below
commit b4646b62a29c2731272d9985186d1e184fa47892
Author: Colin Lee <[email protected]>
AuthorDate: Mon Jul 13 14:35:40 2026 +0800
fix invalid datatype. (#864)
---
cpp/src/reader/qds_without_timegenerator.cc | 19 ++++++-
cpp/src/reader/tsfile_series_scan_iterator.h | 13 +++++
.../tree_view/tsfile_tree_query_by_row_test.cc | 13 +++++
python/tests/test_query_by_row.py | 65 ++++++++++++++++++++++
4 files changed, 107 insertions(+), 3 deletions(-)
diff --git a/cpp/src/reader/qds_without_timegenerator.cc
b/cpp/src/reader/qds_without_timegenerator.cc
index b612e5dc2..7f520f339 100644
--- a/cpp/src/reader/qds_without_timegenerator.cc
+++ b/cpp/src/reader/qds_without_timegenerator.cc
@@ -55,8 +55,14 @@ int QDSWithoutTimeGenerator::init_internal(TsFileIOReader*
io_reader,
std::vector<Path> valid_paths;
std::vector<std::string> column_names;
std::vector<common::TSDataType> data_types;
+ // Data type per valid path, captured from the timeseries index right after
+ // alloc_ssi — while itimeseries_index_ is still live. get_next_tsblock()
+ // may later destroy() the SSI (e.g. when limit==0 yields no TsBlock),
which
+ // clears the index, so this must be recorded up front.
+ std::vector<common::TSDataType> ssi_data_types;
column_names.reserve(origin_path_count);
data_types.reserve(origin_path_count);
+ ssi_data_types.reserve(origin_path_count);
Expression* global_time_expression = qe->expression_;
Filter* global_time_filter = nullptr;
if (global_time_expression != nullptr) {
@@ -91,6 +97,7 @@ int QDSWithoutTimeGenerator::init_internal(TsFileIOReader*
io_reader,
ssi_vec_.push_back(ssi);
valid_paths.push_back(paths[i]);
column_names.push_back(paths[i].full_path_);
+ ssi_data_types.push_back(ssi->get_data_type());
}
size_t path_count = valid_paths.size();
@@ -108,9 +115,15 @@ int QDSWithoutTimeGenerator::init_internal(TsFileIOReader*
io_reader,
for (size_t i = 0; i < path_count; i++) {
get_next_tsblock(i, true);
- data_types.push_back(value_iters_[i] != nullptr
- ? value_iters_[i]->get_data_type()
- : TSDataType::NULL_TYPE);
+ // Prefer the type carried by the value iterator, but fall back to the
+ // timeseries-index type captured before get_next_tsblock() when no
+ // TsBlock was produced (e.g. limit==0 skips every row, or the series
is
+ // empty). Emitting NULL_TYPE here would surface an invalid datatype
+ // (254) to callers that map the metadata onto their own type enums.
+ common::TSDataType col_type = value_iters_[i] != nullptr
+ ? value_iters_[i]->get_data_type()
+ : ssi_data_types[i];
+ data_types.push_back(col_type);
}
// Single-path: SSI may have consumed offset/limit by skipping chunks/pages
// during first get_next_tsblock(); sync so QDS does not double-apply.
diff --git a/cpp/src/reader/tsfile_series_scan_iterator.h
b/cpp/src/reader/tsfile_series_scan_iterator.h
index 77037d8e1..68f1a1f32 100644
--- a/cpp/src/reader/tsfile_series_scan_iterator.h
+++ b/cpp/src/reader/tsfile_series_scan_iterator.h
@@ -105,6 +105,19 @@ class TsFileSeriesScanIterator {
bool is_multi_value() const { return is_multi_value_; }
+ /**
+ * Data type of the (value) column from the loaded timeseries index.
+ * Available as soon as the SSI is allocated, i.e. independent of whether
+ * any TsBlock has been materialized. Callers building result-set metadata
+ * should prefer this over deriving the type from a decoded TsBlock, since
+ * offset/limit (e.g. limit==0) may skip all rows and leave no block.
+ */
+ common::TSDataType get_data_type() const {
+ return itimeseries_index_ == nullptr
+ ? common::INVALID_DATATYPE
+ : itimeseries_index_->get_data_type();
+ }
+
friend class TsFileIOReader;
private:
diff --git a/cpp/test/reader/tree_view/tsfile_tree_query_by_row_test.cc
b/cpp/test/reader/tree_view/tsfile_tree_query_by_row_test.cc
index 1aa1b4623..870b30aaf 100644
--- a/cpp/test/reader/tree_view/tsfile_tree_query_by_row_test.cc
+++ b/cpp/test/reader/tree_view/tsfile_tree_query_by_row_test.cc
@@ -518,6 +518,13 @@ TEST_F(TreeQueryByRowTest, OffsetExceedsTotalRows) {
ASSERT_EQ(E_OK, reader.queryByRow(devices, measurements, 100, -1, result));
ASSERT_NE(result, nullptr);
+ // Even with no rows, the column type must reflect the series' real type
+ // (INT64) rather than NULL_TYPE — downstream bindings map this onto their
+ // own type enums and reject NULL_TYPE (254).
+ auto meta = result->get_metadata();
+ ASSERT_EQ(2u, meta->get_column_count());
+ EXPECT_EQ(INT64, meta->get_column_type(2));
+
auto timestamps = collect_timestamps(result);
EXPECT_EQ(timestamps.size(), 0u);
@@ -539,6 +546,12 @@ TEST_F(TreeQueryByRowTest, LimitZero) {
ASSERT_EQ(E_OK, reader.queryByRow(devices, measurements, 0, 0, result));
ASSERT_NE(result, nullptr);
+ // limit==0 pushes down to the scan iterator and produces no TsBlock; the
+ // metadata must still report the real column type (INT64), not NULL_TYPE.
+ auto meta = result->get_metadata();
+ ASSERT_EQ(2u, meta->get_column_count());
+ EXPECT_EQ(INT64, meta->get_column_type(2));
+
auto timestamps = collect_timestamps(result);
EXPECT_EQ(timestamps.size(), 0u);
diff --git a/python/tests/test_query_by_row.py
b/python/tests/test_query_by_row.py
index c9c993014..468b0a7e9 100644
--- a/python/tests/test_query_by_row.py
+++ b/python/tests/test_query_by_row.py
@@ -172,6 +172,71 @@ def test_query_table_by_row_offset_limit():
os.remove(file_path)
+def test_query_tree_by_row_limit_zero():
+ """limit=0 must return an empty result set with valid column metadata.
+
+ Regression: single-path queries push limit down to the scan iterator, so
+ the first TsBlock is never materialized and the column type used to fall
+ back to NULL_TYPE (254), which broke metadata mapping on the Python side
+ (`ValueError: 254 is not a valid TSDataType`).
+ """
+ file_path = "python_tree_query_by_row_limit_zero.tsfile"
+ if os.path.exists(file_path):
+ os.remove(file_path)
+
+ try:
+ device_id = "root.d1"
+ specs = [
+ ("s1", TSDataType.INT64),
+ ("s2", TSDataType.DOUBLE),
+ ("s3", TSDataType.BOOLEAN),
+ ("s4", TSDataType.STRING),
+ ]
+ num_rows = 10
+
+ writer = TsFileWriter(file_path)
+ for name, dtype in specs:
+ writer.register_timeseries(device_id, TimeseriesSchema(name,
dtype))
+ for t in range(num_rows):
+ fields = [
+ Field("s1", t, TSDataType.INT64),
+ Field("s2", float(t), TSDataType.DOUBLE),
+ Field("s3", t % 2 == 0, TSDataType.BOOLEAN),
+ Field("s4", f"v{t}", TSDataType.STRING),
+ ]
+ writer.write_row_record(RowRecord(device_id, t, fields))
+ writer.close()
+
+ reader = TsFileReader(file_path)
+
+ # Single-path limit=0 for each data type: metadata type must be exact
+ # and no rows must be returned.
+ for name, dtype in specs:
+ with reader.query_tree_by_row([device_id], [name], 0, 0) as result:
+ info = result.get_result_column_info()
+ assert info[f"{device_id}.{name}"] == dtype
+ assert not result.next()
+
+ # Multi-path limit=0: every column keeps its declared type, zero rows.
+ names = [name for name, _ in specs]
+ with reader.query_tree_by_row([device_id], names, 0, 0) as result:
+ info = result.get_result_column_info()
+ for name, dtype in specs:
+ assert info[f"{device_id}.{name}"] == dtype
+ assert not result.next()
+
+ # An offset past the end of the data hits the same no-TsBlock path.
+ with reader.query_tree_by_row([device_id], ["s1"], num_rows + 5, -1)
as result:
+ info = result.get_result_column_info()
+ assert info[f"{device_id}.s1"] == TSDataType.INT64
+ assert not result.next()
+
+ reader.close()
+ finally:
+ if os.path.exists(file_path):
+ os.remove(file_path)
+
+
def test_query_tree_by_row_skips_missing_device_and_measurement():
"""Tree queryByRow: missing device or measurement paths are skipped
(Java-aligned)."""
file_path = "python_tree_query_by_row_skip_missing.tsfile"