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

ColinLeeo 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 218bbdf76 fix(cpp): preserve Gorilla sentinel values in batch reads 
(#883)
218bbdf76 is described below

commit 218bbdf7652ab79d806f53f6fa86111d99618d99
Author: Colin Lee <[email protected]>
AuthorDate: Mon Aug 3 09:52:03 2026 +0800

    fix(cpp): preserve Gorilla sentinel values in batch reads (#883)
    
    * fix(cpp): preserve Gorilla sentinel values in aligned reads
    
    * fix(cpp): handle Gorilla sentinels across chunk readers
---
 cpp/src/encoding/decoder.h              | 103 ++++++++++++++++++
 cpp/src/reader/aligned_chunk_reader.cc  | 180 ++++++++++++++++----------------
 cpp/src/reader/chunk_reader.cc          |  83 +++++++--------
 cpp/test/encoding/gorilla_codec_test.cc |  42 ++++++++
 cpp/test/reader/tsfile_reader_test.cc   | 119 +++++++++++++++++++++
 5 files changed, 393 insertions(+), 134 deletions(-)

diff --git a/cpp/src/encoding/decoder.h b/cpp/src/encoding/decoder.h
index 70b9cde93..035023f8c 100644
--- a/cpp/src/encoding/decoder.h
+++ b/cpp/src/encoding/decoder.h
@@ -155,6 +155,52 @@ class Decoder {
         return common::E_OK;
     }
 
+    // Some encodings use an otherwise valid value as an end marker. Gorilla,
+    // for example, uses canonical NaN for FLOAT/DOUBLE and the minimum value
+    // for INT32/INT64. The regular batch API must honor that marker because it
+    // only receives a capacity. Callers that know the exact value count from
+    // page metadata can use these methods to disambiguate an embedded marker
+    // from the physical end marker.
+    int read_exact_int32(int32_t* out, int count, common::ByteStream& in) {
+        return read_exact_impl(out, count, in, &Decoder::read_batch_int32,
+                               &Decoder::read_int32);
+    }
+
+    int read_exact_int64(int64_t* out, int count, common::ByteStream& in) {
+        return read_exact_impl(out, count, in, &Decoder::read_batch_int64,
+                               &Decoder::read_int64);
+    }
+
+    int read_exact_float(float* out, int count, common::ByteStream& in) {
+        return read_exact_impl(out, count, in, &Decoder::read_batch_float,
+                               &Decoder::read_float);
+    }
+
+    int read_exact_double(double* out, int count, common::ByteStream& in) {
+        return read_exact_impl(out, count, in, &Decoder::read_batch_double,
+                               &Decoder::read_double);
+    }
+
+    int skip_exact_int32(int count, common::ByteStream& in) {
+        return skip_exact_impl<int32_t>(count, in, &Decoder::skip_int32,
+                                        &Decoder::read_int32);
+    }
+
+    int skip_exact_int64(int count, common::ByteStream& in) {
+        return skip_exact_impl<int64_t>(count, in, &Decoder::skip_int64,
+                                        &Decoder::read_int64);
+    }
+
+    int skip_exact_float(int count, common::ByteStream& in) {
+        return skip_exact_impl<float>(count, in, &Decoder::skip_float,
+                                      &Decoder::read_float);
+    }
+
+    int skip_exact_double(int count, common::ByteStream& in) {
+        return skip_exact_impl<double>(count, in, &Decoder::skip_double,
+                                       &Decoder::read_double);
+    }
+
     // Block-level filter pushdown for TS_2DIFF-encoded INT64 columns.
     //
     // TS_2DIFF stores values in self-contained "blocks": a header (value
@@ -188,6 +234,63 @@ class Decoder {
     virtual int skip_peeked_block_int64(common::ByteStream& in, int& skipped) {
         return common::E_NOT_SUPPORT;
     }
+
+   private:
+    template <typename T>
+    int read_exact_impl(T* out, int count, common::ByteStream& in,
+                        int (Decoder::*read_batch)(T*, int, int&,
+                                                   common::ByteStream&),
+                        int (Decoder::*read_one)(T&, common::ByteStream&)) {
+        if (count < 0 || (count > 0 && out == nullptr)) {
+            return common::E_INVALID_ARG;
+        }
+
+        int actual = 0;
+        while (actual < count) {
+            int batch_actual = 0;
+            int ret = (this->*read_batch)(out + actual, count - actual,
+                                          batch_actual, in);
+            if (ret != common::E_OK) return ret;
+            if (batch_actual < 0 || batch_actual > count - actual) {
+                return common::E_TSFILE_CORRUPTED;
+            }
+            actual += batch_actual;
+            if (actual == count) return common::E_OK;
+
+            if ((ret = (this->*read_one)(out[actual], in)) != common::E_OK) {
+                return ret;
+            }
+            ++actual;
+        }
+        return common::E_OK;
+    }
+
+    template <typename T>
+    int skip_exact_impl(int count, common::ByteStream& in,
+                        int (Decoder::*skip_batch)(int, int&,
+                                                   common::ByteStream&),
+                        int (Decoder::*read_one)(T&, common::ByteStream&)) {
+        if (count < 0) return common::E_INVALID_ARG;
+
+        int skipped = 0;
+        while (skipped < count) {
+            int batch_skipped = 0;
+            int ret = (this->*skip_batch)(count - skipped, batch_skipped, in);
+            if (ret != common::E_OK) return ret;
+            if (batch_skipped < 0 || batch_skipped > count - skipped) {
+                return common::E_TSFILE_CORRUPTED;
+            }
+            skipped += batch_skipped;
+            if (skipped == count) return common::E_OK;
+
+            T ignored;
+            if ((ret = (this->*read_one)(ignored, in)) != common::E_OK) {
+                return ret;
+            }
+            ++skipped;
+        }
+        return common::E_OK;
+    }
 };
 
 }  // end namespace storage
diff --git a/cpp/src/reader/aligned_chunk_reader.cc 
b/cpp/src/reader/aligned_chunk_reader.cc
index 795ec6b80..97c469288 100644
--- a/cpp/src/reader/aligned_chunk_reader.cc
+++ b/cpp/src/reader/aligned_chunk_reader.cc
@@ -784,40 +784,39 @@ int AlignedChunkReader::i32_DECODE_TYPED_TV_INTO_TSBLOCK(
 }
 
 namespace {
-// Type-dispatched value batch read / skip for decode_tv_batch<T>.  Overload
-// resolution on the value pointer type selects the matching Decoder method, so
-// the four fixed-width value types share one decode loop.
-FORCE_INLINE int read_value_batch_typed(Decoder* d, int32_t* out, int cap,
-                                        int& actual, ByteStream& in) {
-    return d->read_batch_int32(out, cap, actual, in);
+// Type-dispatched exact read / skip for decode_tv_batch<T>. Overload
+// resolution on the value pointer selects the matching Decoder method.
+FORCE_INLINE int read_value_exact_typed(Decoder* d, int32_t* out, int count,
+                                        ByteStream& in) {
+    return d->read_exact_int32(out, count, in);
 }
-FORCE_INLINE int read_value_batch_typed(Decoder* d, int64_t* out, int cap,
-                                        int& actual, ByteStream& in) {
-    return d->read_batch_int64(out, cap, actual, in);
+FORCE_INLINE int read_value_exact_typed(Decoder* d, int64_t* out, int count,
+                                        ByteStream& in) {
+    return d->read_exact_int64(out, count, in);
 }
-FORCE_INLINE int read_value_batch_typed(Decoder* d, float* out, int cap,
-                                        int& actual, ByteStream& in) {
-    return d->read_batch_float(out, cap, actual, in);
+FORCE_INLINE int read_value_exact_typed(Decoder* d, float* out, int count,
+                                        ByteStream& in) {
+    return d->read_exact_float(out, count, in);
 }
-FORCE_INLINE int read_value_batch_typed(Decoder* d, double* out, int cap,
-                                        int& actual, ByteStream& in) {
-    return d->read_batch_double(out, cap, actual, in);
+FORCE_INLINE int read_value_exact_typed(Decoder* d, double* out, int count,
+                                        ByteStream& in) {
+    return d->read_exact_double(out, count, in);
 }
-FORCE_INLINE int skip_value_typed(Decoder* d, int32_t*, int n, int& skipped,
-                                  ByteStream& in) {
-    return d->skip_int32(n, skipped, in);
+FORCE_INLINE int skip_value_exact_typed(Decoder* d, int32_t*, int count,
+                                        ByteStream& in) {
+    return d->skip_exact_int32(count, in);
 }
-FORCE_INLINE int skip_value_typed(Decoder* d, int64_t*, int n, int& skipped,
-                                  ByteStream& in) {
-    return d->skip_int64(n, skipped, in);
+FORCE_INLINE int skip_value_exact_typed(Decoder* d, int64_t*, int count,
+                                        ByteStream& in) {
+    return d->skip_exact_int64(count, in);
 }
-FORCE_INLINE int skip_value_typed(Decoder* d, float*, int n, int& skipped,
-                                  ByteStream& in) {
-    return d->skip_float(n, skipped, in);
+FORCE_INLINE int skip_value_exact_typed(Decoder* d, float*, int count,
+                                        ByteStream& in) {
+    return d->skip_exact_float(count, in);
 }
-FORCE_INLINE int skip_value_typed(Decoder* d, double*, int n, int& skipped,
-                                  ByteStream& in) {
-    return d->skip_double(n, skipped, in);
+FORCE_INLINE int skip_value_exact_typed(Decoder* d, double*, int count,
+                                        ByteStream& in) {
+    return d->skip_exact_double(count, in);
 }
 }  // namespace
 
@@ -864,17 +863,8 @@ int AlignedChunkReader::decode_tv_batch(ByteStream& 
time_in,
                     }
                     cur_value_index += block_count;
                     if (nonnull > 0) {
-                        // skip_* may legitimately fail (truncated page) or
-                        // short-read (corrupt bitmap vs. data); both must 
abort
-                        // the loop rather than silently desync the value
-                        // decoder.
-                        int sk = 0;
-                        if (RET_FAIL(skip_value_typed(value_decoder_, values,
-                                                      nonnull, sk, value_in))) 
{
-                            break;
-                        }
-                        if (sk != nonnull) {
-                            ret = E_TSFILE_CORRUPTED;
+                        if (RET_FAIL(skip_value_exact_typed(
+                                value_decoder_, values, nonnull, value_in))) {
                             break;
                         }
                     }
@@ -916,14 +906,8 @@ int AlignedChunkReader::decode_tv_batch(ByteStream& 
time_in,
 
         if (pass_count == 0) {
             if (nonnull_count > 0) {
-                int skipped = 0;
-                if (RET_FAIL(skip_value_typed(value_decoder_, values,
-                                              nonnull_count, skipped,
-                                              value_in))) {
-                    break;
-                }
-                if (skipped != nonnull_count) {
-                    ret = E_TSFILE_CORRUPTED;
+                if (RET_FAIL(skip_value_exact_typed(value_decoder_, values,
+                                                    nonnull_count, value_in))) 
{
                     break;
                 }
             }
@@ -931,11 +915,9 @@ int AlignedChunkReader::decode_tv_batch(ByteStream& 
time_in,
             continue;
         }
 
-        int value_count = 0;
         if (nonnull_count > 0) {
-            if (RET_FAIL(read_value_batch_typed(value_decoder_, values,
-                                                nonnull_count, value_count,
-                                                value_in))) {
+            if (RET_FAIL(read_value_exact_typed(value_decoder_, values,
+                                                nonnull_count, value_in))) {
                 break;
             }
         }
@@ -1704,7 +1686,6 @@ int 
AlignedChunkReader::decode_value_page_for_slot(uint32_t col_idx,
     uint32_t elem_size = common::get_data_type_size(dt);
     pps.predecoded_values.resize(static_cast<size_t>(nonnull_total) *
                                  elem_size);
-    int actual = 0;
     switch (dt) {
         case common::BOOLEAN: {
             bool* out = reinterpret_cast<bool*>(pps.predecoded_values.data());
@@ -1714,39 +1695,38 @@ int 
AlignedChunkReader::decode_value_page_for_slot(uint32_t col_idx,
                     return ret;
                 }
             }
-            actual = nonnull_total;
             break;
         }
         case common::INT32:
         case common::DATE:
-            if (RET_FAIL(col->decoder->read_batch_int32(
+            if (RET_FAIL(col->decoder->read_exact_int32(
                     reinterpret_cast<int32_t*>(pps.predecoded_values.data()),
-                    nonnull_total, actual, in))) {
+                    nonnull_total, in))) {
                 cleanup();
                 return ret;
             }
             break;
         case common::INT64:
         case common::TIMESTAMP:
-            if (RET_FAIL(col->decoder->read_batch_int64(
+            if (RET_FAIL(col->decoder->read_exact_int64(
                     reinterpret_cast<int64_t*>(pps.predecoded_values.data()),
-                    nonnull_total, actual, in))) {
+                    nonnull_total, in))) {
                 cleanup();
                 return ret;
             }
             break;
         case common::FLOAT:
-            if (RET_FAIL(col->decoder->read_batch_float(
+            if (RET_FAIL(col->decoder->read_exact_float(
                     reinterpret_cast<float*>(pps.predecoded_values.data()),
-                    nonnull_total, actual, in))) {
+                    nonnull_total, in))) {
                 cleanup();
                 return ret;
             }
             break;
         case common::DOUBLE:
-            if (RET_FAIL(col->decoder->read_batch_double(
+            if (RET_FAIL(col->decoder->read_exact_double(
                     reinterpret_cast<double*>(pps.predecoded_values.data()),
-                    nonnull_total, actual, in))) {
+                    nonnull_total, in))) {
                 cleanup();
                 return ret;
             }
@@ -1755,7 +1735,7 @@ int 
AlignedChunkReader::decode_value_page_for_slot(uint32_t col_idx,
             cleanup();
             return E_NOT_SUPPORT;
     }
-    pps.predecoded_count = actual;
+    pps.predecoded_count = nonnull_total;
     cleanup();
     return E_OK;
 }
@@ -2372,35 +2352,39 @@ int 
AlignedChunkReader::decompress_and_parse_value_page(ValueColumnState& col,
                             break;
                         }
                         out[i] = v;
+                        actual++;
                     }
-                    actual = nonnull_total;
                     break;
                 }
                 case common::INT32:
                 case common::DATE:
-                    rret = col.decoder->read_batch_int32(
+                    rret = col.decoder->read_exact_int32(
                         reinterpret_cast<int32_t*>(
                             col.pending_decoded_values.data()),
-                        nonnull_total, actual, col.in);
+                        nonnull_total, col.in);
+                    if (rret == common::E_OK) actual = nonnull_total;
                     break;
                 case common::INT64:
                 case common::TIMESTAMP:
-                    rret = col.decoder->read_batch_int64(
+                    rret = col.decoder->read_exact_int64(
                         reinterpret_cast<int64_t*>(
                             col.pending_decoded_values.data()),
-                        nonnull_total, actual, col.in);
+                        nonnull_total, col.in);
+                    if (rret == common::E_OK) actual = nonnull_total;
                     break;
                 case common::FLOAT:
-                    rret = col.decoder->read_batch_float(
+                    rret = col.decoder->read_exact_float(
                         reinterpret_cast<float*>(
                             col.pending_decoded_values.data()),
-                        nonnull_total, actual, col.in);
+                        nonnull_total, col.in);
+                    if (rret == common::E_OK) actual = nonnull_total;
                     break;
                 case common::DOUBLE:
-                    rret = col.decoder->read_batch_double(
+                    rret = col.decoder->read_exact_double(
                         reinterpret_cast<double*>(
                             col.pending_decoded_values.data()),
-                        nonnull_total, actual, col.in);
+                        nonnull_total, col.in);
+                    if (rret == common::E_OK) actual = nonnull_total;
                     break;
                 default:
                     rret = common::E_OUT_OF_RANGE;
@@ -2524,11 +2508,9 @@ int AlignedChunkReader::multi_DECODE_TV_BATCH(TsBlock* 
ret_tsblock,
                 }
             }
 
-            // Skip values if no rows pass time filter.  Skip/read errors and
-            // short reads (decoder returned fewer values than the bitmap
-            // promised) must abort; otherwise the input stream is left
-            // mid-value and later batches would decode garbage from
-            // misaligned bytes.
+            // Skip values if no rows pass time filter.  The aligned bitmap
+            // supplies the exact count, including values equal to a codec's
+            // in-band terminator.
             if (pass_count == 0 && cb.nonnull_count > 0) {
                 int dret = common::E_OK;
                 int sk = 0;
@@ -2543,21 +2525,29 @@ int AlignedChunkReader::multi_DECODE_TV_BATCH(TsBlock* 
ret_tsblock,
                     }
                     case common::INT32:
                     case common::DATE:
-                        dret = col->decoder->skip_int32(cb.nonnull_count, sk,
-                                                        col->in);
+                        dret = skip_value_exact_typed(
+                            col->decoder, static_cast<int32_t*>(nullptr),
+                            cb.nonnull_count, col->in);
+                        if (dret == common::E_OK) sk = cb.nonnull_count;
                         break;
                     case common::INT64:
                     case common::TIMESTAMP:
-                        dret = col->decoder->skip_int64(cb.nonnull_count, sk,
-                                                        col->in);
+                        dret = skip_value_exact_typed(
+                            col->decoder, static_cast<int64_t*>(nullptr),
+                            cb.nonnull_count, col->in);
+                        if (dret == common::E_OK) sk = cb.nonnull_count;
                         break;
                     case common::FLOAT:
-                        dret = col->decoder->skip_float(cb.nonnull_count, sk,
-                                                        col->in);
+                        dret = skip_value_exact_typed(
+                            col->decoder, static_cast<float*>(nullptr),
+                            cb.nonnull_count, col->in);
+                        if (dret == common::E_OK) sk = cb.nonnull_count;
                         break;
                     case common::DOUBLE:
-                        dret = col->decoder->skip_double(cb.nonnull_count, sk,
-                                                         col->in);
+                        dret = skip_value_exact_typed(
+                            col->decoder, static_cast<double*>(nullptr),
+                            cb.nonnull_count, col->in);
+                        if (dret == common::E_OK) sk = cb.nonnull_count;
                         break;
                     case common::STRING:
                     case common::TEXT:
@@ -2621,25 +2611,33 @@ int AlignedChunkReader::multi_DECODE_TV_BATCH(TsBlock* 
ret_tsblock,
                         }
                         case common::INT32:
                         case common::DATE:
-                            dret = col->decoder->read_batch_int32(
+                            dret = col->decoder->read_exact_int32(
                                 reinterpret_cast<int32_t*>(cb.val_buf),
-                                cb.nonnull_count, cb.val_count, col->in);
+                                cb.nonnull_count, col->in);
+                            if (dret == common::E_OK)
+                                cb.val_count = cb.nonnull_count;
                             break;
                         case common::INT64:
                         case common::TIMESTAMP:
-                            dret = col->decoder->read_batch_int64(
+                            dret = col->decoder->read_exact_int64(
                                 reinterpret_cast<int64_t*>(cb.val_buf),
-                                cb.nonnull_count, cb.val_count, col->in);
+                                cb.nonnull_count, col->in);
+                            if (dret == common::E_OK)
+                                cb.val_count = cb.nonnull_count;
                             break;
                         case common::FLOAT:
-                            dret = col->decoder->read_batch_float(
+                            dret = col->decoder->read_exact_float(
                                 reinterpret_cast<float*>(cb.val_buf),
-                                cb.nonnull_count, cb.val_count, col->in);
+                                cb.nonnull_count, col->in);
+                            if (dret == common::E_OK)
+                                cb.val_count = cb.nonnull_count;
                             break;
                         case common::DOUBLE:
-                            dret = col->decoder->read_batch_double(
+                            dret = col->decoder->read_exact_double(
                                 reinterpret_cast<double*>(cb.val_buf),
-                                cb.nonnull_count, cb.val_count, col->in);
+                                cb.nonnull_count, col->in);
+                            if (dret == common::E_OK)
+                                cb.val_count = cb.nonnull_count;
                             break;
                         case common::STRING:
                         case common::TEXT:
diff --git a/cpp/src/reader/chunk_reader.cc b/cpp/src/reader/chunk_reader.cc
index ce8d87a58..b21b1b4c5 100644
--- a/cpp/src/reader/chunk_reader.cc
+++ b/cpp/src/reader/chunk_reader.cc
@@ -479,7 +479,10 @@ int ChunkReader::i32_DECODE_TV_BATCH(ByteStream& time_in, 
ByteStream& value_in,
                 if (!filter->satisfy_start_end_time(block_min, block_max)) {
                     int skipped = 0;
                     time_decoder_->skip_peeked_block_int64(time_in, skipped);
-                    value_decoder_->skip_int32(block_count, skipped, value_in);
+                    if (RET_FAIL(value_decoder_->skip_exact_int32(block_count,
+                                                                  value_in))) {
+                        break;
+                    }
                     continue;
                 }
                 if (filter->contain_start_end_time(block_min, block_max)) {
@@ -489,7 +492,6 @@ int ChunkReader::i32_DECODE_TV_BATCH(ByteStream& time_in, 
ByteStream& value_in,
         }
 
         int time_count = 0;
-        int value_count = 0;
 
         if (RET_FAIL(time_decoder_->read_batch_int64(times, eff_batch,
                                                      time_count, time_in))) {
@@ -505,20 +507,15 @@ int ChunkReader::i32_DECODE_TV_BATCH(ByteStream& time_in, 
ByteStream& value_in,
         }
 
         if (pass_count == 0) {
-            int skipped = 0;
-            value_decoder_->skip_int32(time_count, skipped, value_in);
+            if (RET_FAIL(
+                    value_decoder_->skip_exact_int32(time_count, value_in))) {
+                break;
+            }
             continue;
         }
 
-        if (RET_FAIL(value_decoder_->read_batch_int32(values, time_count,
-                                                      value_count, value_in))) 
{
-            break;
-        }
-        // Time and value chunks are written in lock-step; any discrepancy
-        // means the file is truncated or corrupted.  Reading uninitialised
-        // values[i] would silently surface garbage as decoded rows.
-        if (value_count != time_count) {
-            ret = E_TSFILE_CORRUPTED;
+        if (RET_FAIL(value_decoder_->read_exact_int32(values, time_count,
+                                                      value_in))) {
             break;
         }
 
@@ -568,7 +565,10 @@ int ChunkReader::i64_DECODE_TV_BATCH(ByteStream& time_in, 
ByteStream& value_in,
                 if (!filter->satisfy_start_end_time(block_min, block_max)) {
                     int skipped = 0;
                     time_decoder_->skip_peeked_block_int64(time_in, skipped);
-                    value_decoder_->skip_int64(block_count, skipped, value_in);
+                    if (RET_FAIL(value_decoder_->skip_exact_int64(block_count,
+                                                                  value_in))) {
+                        break;
+                    }
                     continue;
                 }
                 if (filter->contain_start_end_time(block_min, block_max)) {
@@ -578,7 +578,6 @@ int ChunkReader::i64_DECODE_TV_BATCH(ByteStream& time_in, 
ByteStream& value_in,
         }
 
         int time_count = 0;
-        int value_count = 0;
 
         if (RET_FAIL(time_decoder_->read_batch_int64(times, eff_batch,
                                                      time_count, time_in))) {
@@ -594,17 +593,15 @@ int ChunkReader::i64_DECODE_TV_BATCH(ByteStream& time_in, 
ByteStream& value_in,
         }
 
         if (pass_count == 0) {
-            int skipped = 0;
-            value_decoder_->skip_int64(time_count, skipped, value_in);
+            if (RET_FAIL(
+                    value_decoder_->skip_exact_int64(time_count, value_in))) {
+                break;
+            }
             continue;
         }
 
-        if (RET_FAIL(value_decoder_->read_batch_int64(values, time_count,
-                                                      value_count, value_in))) 
{
-            break;
-        }
-        if (value_count != time_count) {
-            ret = E_TSFILE_CORRUPTED;
+        if (RET_FAIL(value_decoder_->read_exact_int64(values, time_count,
+                                                      value_in))) {
             break;
         }
 
@@ -655,7 +652,10 @@ int ChunkReader::float_DECODE_TV_BATCH(ByteStream& time_in,
                 if (!filter->satisfy_start_end_time(block_min, block_max)) {
                     int skipped = 0;
                     time_decoder_->skip_peeked_block_int64(time_in, skipped);
-                    value_decoder_->skip_float(block_count, skipped, value_in);
+                    if (RET_FAIL(value_decoder_->skip_exact_float(block_count,
+                                                                  value_in))) {
+                        break;
+                    }
                     continue;
                 }
                 if (filter->contain_start_end_time(block_min, block_max)) {
@@ -665,7 +665,6 @@ int ChunkReader::float_DECODE_TV_BATCH(ByteStream& time_in,
         }
 
         int time_count = 0;
-        int value_count = 0;
 
         if (RET_FAIL(time_decoder_->read_batch_int64(times, eff_batch,
                                                      time_count, time_in))) {
@@ -681,17 +680,15 @@ int ChunkReader::float_DECODE_TV_BATCH(ByteStream& 
time_in,
         }
 
         if (pass_count == 0) {
-            int skipped = 0;
-            value_decoder_->skip_float(time_count, skipped, value_in);
+            if (RET_FAIL(
+                    value_decoder_->skip_exact_float(time_count, value_in))) {
+                break;
+            }
             continue;
         }
 
-        if (RET_FAIL(value_decoder_->read_batch_float(values, time_count,
-                                                      value_count, value_in))) 
{
-            break;
-        }
-        if (value_count != time_count) {
-            ret = E_TSFILE_CORRUPTED;
+        if (RET_FAIL(value_decoder_->read_exact_float(values, time_count,
+                                                      value_in))) {
             break;
         }
 
@@ -738,7 +735,10 @@ int ChunkReader::double_DECODE_TV_BATCH(ByteStream& 
time_in,
                 if (!filter->satisfy_start_end_time(block_min, block_max)) {
                     int skipped = 0;
                     time_decoder_->skip_peeked_block_int64(time_in, skipped);
-                    value_decoder_->skip_double(block_count, skipped, 
value_in);
+                    if (RET_FAIL(value_decoder_->skip_exact_double(block_count,
+                                                                   value_in))) 
{
+                        break;
+                    }
                     continue;
                 }
                 if (filter->contain_start_end_time(block_min, block_max)) {
@@ -748,7 +748,6 @@ int ChunkReader::double_DECODE_TV_BATCH(ByteStream& time_in,
         }
 
         int time_count = 0;
-        int value_count = 0;
 
         if (RET_FAIL(time_decoder_->read_batch_int64(times, eff_batch,
                                                      time_count, time_in))) {
@@ -764,17 +763,15 @@ int ChunkReader::double_DECODE_TV_BATCH(ByteStream& 
time_in,
         }
 
         if (pass_count == 0) {
-            int skipped = 0;
-            value_decoder_->skip_double(time_count, skipped, value_in);
+            if (RET_FAIL(
+                    value_decoder_->skip_exact_double(time_count, value_in))) {
+                break;
+            }
             continue;
         }
 
-        if (RET_FAIL(value_decoder_->read_batch_double(
-                values, time_count, value_count, value_in))) {
-            break;
-        }
-        if (value_count != time_count) {
-            ret = E_TSFILE_CORRUPTED;
+        if (RET_FAIL(value_decoder_->read_exact_double(values, time_count,
+                                                       value_in))) {
             break;
         }
 
diff --git a/cpp/test/encoding/gorilla_codec_test.cc 
b/cpp/test/encoding/gorilla_codec_test.cc
index b1fe72136..039a9a4f0 100644
--- a/cpp/test/encoding/gorilla_codec_test.cc
+++ b/cpp/test/encoding/gorilla_codec_test.cc
@@ -486,6 +486,48 @@ TEST_F(GorillaCodecTest, 
DoubleBatchScalarAndSkipInterleave) {
     EXPECT_EQ(cursor, N);
 }
 
+TEST_F(GorillaCodecTest, DoubleExactReadAndSkipPreserveEmbeddedNaN) {
+    const double expected[] = {1.25, std::nan(""), 2.5, std::nan(""), 5.0};
+    const int count = sizeof(expected) / sizeof(expected[0]);
+
+    storage::DoubleGorillaEncoder encoder;
+    common::ByteStream stream(1024, common::MOD_DEFAULT);
+    for (double value : expected) {
+        ASSERT_EQ(encoder.encode(value, stream), common::E_OK);
+    }
+    encoder.flush(stream);
+
+    const uint32_t total = stream.total_size();
+    std::vector<uint8_t> buf(total);
+    uint32_t got = 0;
+    stream.read_buf(buf.data(), total, got);
+    ASSERT_EQ(got, total);
+
+    common::ByteStream read_stream(common::MOD_DEFAULT);
+    read_stream.wrap_from(reinterpret_cast<const char*>(buf.data()), total);
+    storage::DoubleGorillaDecoder read_decoder;
+    double actual[count];
+    ASSERT_EQ(read_decoder.read_exact_double(actual, count, read_stream),
+              common::E_OK);
+    for (int i = 0; i < count; i++) {
+        if (std::isnan(expected[i])) {
+            EXPECT_TRUE(std::isnan(actual[i])) << "i=" << i;
+        } else {
+            EXPECT_DOUBLE_EQ(actual[i], expected[i]) << "i=" << i;
+        }
+    }
+
+    common::ByteStream skip_stream(common::MOD_DEFAULT);
+    skip_stream.wrap_from(reinterpret_cast<const char*>(buf.data()), total);
+    storage::DoubleGorillaDecoder skip_decoder;
+    ASSERT_EQ(skip_decoder.skip_exact_double(3, skip_stream), common::E_OK);
+    double tail[2];
+    ASSERT_EQ(skip_decoder.read_exact_double(tail, 2, skip_stream),
+              common::E_OK);
+    EXPECT_TRUE(std::isnan(tail[0]));
+    EXPECT_DOUBLE_EQ(tail[1], 5.0);
+}
+
 TEST_F(GorillaCodecTest, DoubleBatchDecodeFullWidthXor) {
     const uint64_t patterns[] = {
         0x0000000000000000ULL, 0xFFFFFFFFFFFFFFFFULL, 0x0123456789ABCDEFULL,
diff --git a/cpp/test/reader/tsfile_reader_test.cc 
b/cpp/test/reader/tsfile_reader_test.cc
index f7df9c8c9..cdb6d33cf 100644
--- a/cpp/test/reader/tsfile_reader_test.cc
+++ b/cpp/test/reader/tsfile_reader_test.cc
@@ -21,6 +21,7 @@
 #include <gtest/gtest.h>
 #include <sys/stat.h>
 
+#include <cmath>
 #include <map>
 #include <random>
 #include <unordered_map>
@@ -1343,6 +1344,124 @@ TEST_F(TsFileReaderTest, 
MultiValueAlignedWideChunkParallelDecode) {
     io_reader.revert_ssi(ssi);
 }
 
+// Regression: Gorilla uses the canonical NaN bit pattern as its stream
+// terminator. When the same bit pattern appears as a value, count-aware reads
+// must preserve it and every following value instead of stopping early.
+TEST_F(TsFileReaderTest, GorillaDoubleNaNPreservedByChunkReaders) {
+    const std::string aligned_device = "root.dev_aligned_gorilla_nan";
+    const std::string non_aligned_device = "root.dev_gorilla_nan";
+    std::vector<MeasurementSchema> schema_vec;
+    schema_vec.emplace_back("v0", DOUBLE, GORILLA, UNCOMPRESSED);
+    schema_vec.emplace_back("v1", DOUBLE, GORILLA, UNCOMPRESSED);
+    ASSERT_EQ(
+        tsfile_writer_->register_timeseries(non_aligned_device, schema_vec[0]),
+        E_OK);
+    {
+        std::vector<MeasurementSchema*> registered;
+        for (const auto& schema : schema_vec) {
+            registered.push_back(new MeasurementSchema(schema));
+        }
+        ASSERT_EQ(tsfile_writer_->register_aligned_timeseries(aligned_device,
+                                                              registered),
+                  E_OK);
+    }
+
+    constexpr int kRowCount = 32;
+    constexpr int kNaNRow = 7;
+    Tablet aligned_tablet(
+        aligned_device,
+        std::make_shared<std::vector<MeasurementSchema>>(schema_vec),
+        kRowCount);
+    auto non_aligned_schema =
+        std::make_shared<std::vector<MeasurementSchema>>(1, schema_vec[0]);
+    Tablet non_aligned_tablet(non_aligned_device, non_aligned_schema,
+                              kRowCount);
+    for (int row = 0; row < kRowCount; row++) {
+        ASSERT_EQ(aligned_tablet.add_timestamp(row, row), E_OK);
+        ASSERT_EQ(non_aligned_tablet.add_timestamp(row, row), E_OK);
+        const double v0 = row == kNaNRow ? std::nan("") : 1000.0 + row;
+        ASSERT_EQ(aligned_tablet.add_value(row, 0u, v0), E_OK);
+        ASSERT_EQ(aligned_tablet.add_value(row, 1u, 2000.0 + row), E_OK);
+        ASSERT_EQ(non_aligned_tablet.add_value(row, 0u, v0), E_OK);
+    }
+    ASSERT_EQ(tsfile_writer_->write_tablet(non_aligned_tablet), E_OK);
+    ASSERT_EQ(tsfile_writer_->write_tablet_aligned(aligned_tablet), E_OK);
+    ASSERT_EQ(tsfile_writer_->flush(), E_OK);
+    ASSERT_EQ(tsfile_writer_->close(), E_OK);
+
+    storage::TsFileIOReader io_reader;
+    ASSERT_EQ(io_reader.init(file_name_), E_OK);
+
+    auto check_values = [&](storage::TsFileSeriesScanIterator* ssi,
+                            bool has_v1) {
+        common::TsBlock* block = nullptr;
+        ASSERT_EQ(ssi->get_next(block, /*alloc_tsblock=*/true), E_OK);
+        ASSERT_NE(block, nullptr);
+        ASSERT_EQ(block->get_row_count(), kRowCount);
+
+        {
+            common::ColIterator time_iter(0, block);
+            common::ColIterator v0_iter(1, block);
+            std::unique_ptr<common::ColIterator> v1_iter;
+            if (has_v1) {
+                v1_iter.reset(new common::ColIterator(2, block));
+            }
+            for (int row = 0; row < kRowCount; row++) {
+                uint32_t len = 0;
+                const int64_t time =
+                    *reinterpret_cast<int64_t*>(time_iter.read(&len));
+                const double v0 =
+                    *reinterpret_cast<double*>(v0_iter.read(&len));
+                EXPECT_EQ(time, row);
+                if (row == kNaNRow) {
+                    EXPECT_TRUE(std::isnan(v0));
+                } else {
+                    EXPECT_DOUBLE_EQ(v0, 1000.0 + row);
+                }
+                if (has_v1) {
+                    const double v1 =
+                        *reinterpret_cast<double*>(v1_iter->read(&len));
+                    EXPECT_DOUBLE_EQ(v1, 2000.0 + row);
+                    v1_iter->next();
+                }
+                time_iter.next();
+                v0_iter.next();
+            }
+        }
+        ssi->revert_tsblock();
+
+        block = nullptr;
+        EXPECT_EQ(ssi->get_next(block, /*alloc_tsblock=*/true), 
E_NO_MORE_DATA);
+    };
+
+    {
+        storage::TsFileSeriesScanIterator* ssi = nullptr;
+        common::PageArena pa;
+        pa.init(512, common::MOD_TSFILE_READER);
+        auto device_id =
+            std::make_shared<StringArrayDeviceID>(non_aligned_device);
+        ASSERT_EQ(io_reader.alloc_ssi(device_id, "v0", ssi, pa,
+                                      /*time_filter=*/nullptr),
+                  E_OK);
+        ASSERT_NE(ssi, nullptr);
+        check_values(ssi, /*has_v1=*/false);
+        io_reader.revert_ssi(ssi);
+    }
+
+    {
+        storage::TsFileSeriesScanIterator* ssi = nullptr;
+        common::PageArena pa;
+        pa.init(512, common::MOD_TSFILE_READER);
+        auto device_id = std::make_shared<StringArrayDeviceID>(aligned_device);
+        ASSERT_EQ(io_reader.alloc_multi_ssi(device_id, {"v0", "v1"}, ssi, pa,
+                                            /*time_filter=*/nullptr),
+                  E_OK);
+        ASSERT_NE(ssi, nullptr);
+        check_values(ssi, /*has_v1=*/true);
+        io_reader.revert_ssi(ssi);
+    }
+}
+
 // Regression: AlignedTimeseriesIndex::get_data_type() returns the time column
 // type (VECTOR), which the schema accessor used to surface verbatim — every
 // aligned column came back as VECTOR instead of its real INT32/FLOAT/etc.

Reply via email to