You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tsfile.apache.org by co...@apache.org on 2026/07/13 04:15:17 UTC

(tsfile) branch fix_invalid_datatype created (now 39ce52947)

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

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


      at 39ce52947 fix invalid datatype.

This branch includes the following new commits:

     new 39ce52947 fix invalid datatype.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(tsfile) 01/01: fix invalid datatype.

Posted by co...@apache.org.
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 <sh...@163.com>
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"