This is an automated email from the ASF dual-hosted git repository.

ColinLeeo pushed a commit to branch fix_invalid_datatype
in repository https://gitbox.apache.org/repos/asf/tsfile.git

commit 39ce52947f0081370daf60ebad43f9d6ec371b1f
Author: ColinLee <[email protected]>
AuthorDate: Mon Jul 13 12:14:49 2026 +0800

    fix invalid datatype.
---
 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"

Reply via email to