| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * License); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License a |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| #include <gtest/gtest.h> |
| |
| #include <bitset> |
| #include <chrono> |
| #include <cmath> |
| #include <cstring> |
| #include <iomanip> |
| #include <random> |
| #include <sstream> |
| #include <vector> |
| |
| #include "encoding/ts2diff_decoder.h" |
| #include "encoding/ts2diff_encoder.h" |
| |
| namespace storage { |
| |
| class TS2DIFFCodecTest : public ::testing::Test { |
| protected: |
| void SetUp() override { |
| encoder_int_ = new IntTS2DIFFEncoder(); |
| encoder_long_ = new LongTS2DIFFEncoder(); |
| decoder_int_ = new IntTS2DIFFDecoder(); |
| decoder_long_ = new LongTS2DIFFDecoder(); |
| } |
| |
| void TearDown() override { |
| if (encoder_int_ != nullptr) { |
| encoder_int_->destroy(); |
| delete encoder_int_; |
| encoder_int_ = nullptr; |
| } |
| if (encoder_long_ != nullptr) { |
| encoder_long_->destroy(); |
| delete encoder_long_; |
| encoder_long_ = nullptr; |
| } |
| |
| delete decoder_int_; |
| decoder_int_ = nullptr; |
| delete decoder_long_; |
| decoder_long_ = nullptr; |
| } |
| |
| IntTS2DIFFEncoder* encoder_int_; |
| LongTS2DIFFEncoder* encoder_long_; |
| IntTS2DIFFDecoder* decoder_int_; |
| LongTS2DIFFDecoder* decoder_long_; |
| }; |
| |
| class FloatDoubleTS2DIFFCodecTest : public ::testing::Test { |
| protected: |
| void SetUp() override { |
| encoder_float_ = new FloatTS2DIFFEncoder(); |
| decoder_float_ = new FloatTS2DIFFDecoder(); |
| encoder_double_ = new DoubleTS2DIFFEncoder(); |
| decoder_double_ = new DoubleTS2DIFFDecoder(); |
| } |
| |
| void TearDown() override { |
| if (encoder_float_ != nullptr) { |
| encoder_float_->destroy(); |
| delete encoder_float_; |
| encoder_float_ = nullptr; |
| } |
| if (encoder_double_ != nullptr) { |
| encoder_double_->destroy(); |
| delete encoder_double_; |
| encoder_double_ = nullptr; |
| } |
| delete decoder_float_; |
| decoder_float_ = nullptr; |
| delete decoder_double_; |
| decoder_double_ = nullptr; |
| } |
| |
| FloatTS2DIFFEncoder* encoder_float_{nullptr}; |
| DoubleTS2DIFFEncoder* encoder_double_{nullptr}; |
| FloatTS2DIFFDecoder* decoder_float_{nullptr}; |
| DoubleTS2DIFFDecoder* decoder_double_{nullptr}; |
| }; |
| |
| // Reads the whole stream into a hex string. Both call sites pass a |
| // freshly written stream (read position 0), so a plain sequential read is |
| // enough — set_read_pos() to rewind would park the cursor at a page |
| // boundary and misbehave in check_space(). |
| static std::string byte_stream_to_hex(common::ByteStream& stream) { |
| uint32_t size = stream.total_size(); |
| std::vector<uint8_t> buf(size); |
| uint32_t read_len = 0; |
| EXPECT_EQ(stream.read_buf(buf.data(), size, read_len), common::E_OK); |
| EXPECT_EQ(read_len, size); |
| |
| std::ostringstream oss; |
| for (uint32_t i = 0; i < size; i++) { |
| if (i > 0) { |
| oss << " "; |
| } |
| oss << std::uppercase << std::hex << std::setw(2) << std::setfill('0') |
| << static_cast<unsigned>(buf[i]); |
| } |
| return oss.str(); |
| } |
| |
| TEST_F(FloatDoubleTS2DIFFCodecTest, TestFloatRoundTrip) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| const int row_num = 1000; |
| std::vector<float> data(row_num); |
| for (int i = 0; i < row_num; i++) { |
| data[i] = static_cast<float>(i) * 2.0f + 1.0f; |
| } |
| for (int i = 0; i < row_num; i++) { |
| EXPECT_EQ(encoder_float_->encode(data[i], out_stream), common::E_OK); |
| } |
| EXPECT_EQ(encoder_float_->flush(out_stream), common::E_OK); |
| |
| float x = 0.f; |
| for (int i = 0; i < row_num; i++) { |
| EXPECT_EQ(decoder_float_->read_float(x, out_stream), common::E_OK); |
| EXPECT_FLOAT_EQ(x, data[i]) << "row " << i; |
| } |
| EXPECT_FALSE(decoder_float_->has_remaining(out_stream)); |
| } |
| |
| TEST_F(FloatDoubleTS2DIFFCodecTest, TestFloatJavaDefaultHexCompatibility) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| const float data[] = {3.123456768E20f, std::nanf("")}; |
| |
| for (float v : data) { |
| EXPECT_EQ(encoder_float_->encode(v, out_stream), common::E_OK); |
| } |
| EXPECT_EQ(encoder_float_->flush(out_stream), common::E_OK); |
| |
| // Golden bytes from the Java FloatEncoder + TS_2DIFF writer at the |
| // default maxPointNumber = 2 (TSFileConfig.floatPrecision). |
| const std::string expected_hex = |
| "FE FF FF FF 07 02 00 03 02 00 00 00 01 00 00 00 00 1E 38 8A AA 61 87 " |
| "75 56"; |
| EXPECT_EQ(byte_stream_to_hex(out_stream), expected_hex); |
| } |
| |
| TEST_F(FloatDoubleTS2DIFFCodecTest, TestDoubleJavaDefaultHexCompatibility) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| const double data[] = {3.123456768E20, std::nan("")}; |
| |
| for (double v : data) { |
| EXPECT_EQ(encoder_double_->encode(v, out_stream), common::E_OK); |
| } |
| EXPECT_EQ(encoder_double_->flush(out_stream), common::E_OK); |
| |
| // Golden bytes from the Java DoubleEncoder + TS_2DIFF writer at the |
| // default maxPointNumber = 2 (TSFileConfig.floatPrecision). |
| const std::string expected_hex = |
| "FE FF FF FF 07 02 00 03 02 00 00 00 01 00 00 00 00 3B C7 11 55 3D " |
| "D4 27 08 44 30 EE AA C2 2B D8 F8"; |
| EXPECT_EQ(byte_stream_to_hex(out_stream), expected_hex); |
| } |
| |
| TEST_F(FloatDoubleTS2DIFFCodecTest, TestDoubleRoundTrip) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| const int row_num = 800; |
| std::vector<double> data(row_num); |
| for (int i = 0; i < row_num; i++) { |
| data[i] = static_cast<double>(i) * 2.0 + 1.0; |
| } |
| for (int i = 0; i < row_num; i++) { |
| EXPECT_EQ(encoder_double_->encode(data[i], out_stream), common::E_OK); |
| } |
| EXPECT_EQ(encoder_double_->flush(out_stream), common::E_OK); |
| |
| double y = 0.; |
| for (int i = 0; i < row_num; i++) { |
| EXPECT_EQ(decoder_double_->read_double(y, out_stream), common::E_OK); |
| EXPECT_DOUBLE_EQ(y, data[i]) << "row " << i; |
| } |
| EXPECT_FALSE(decoder_double_->has_remaining(out_stream)); |
| } |
| |
| TEST_F(FloatDoubleTS2DIFFCodecTest, |
| ReadBatchFloatConsumesPrefixesAcrossSegments) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| const int row_num = 300; |
| std::vector<float> expected(row_num); |
| for (int i = 0; i < row_num; ++i) { |
| expected[i] = static_cast<float>(i) * 2.0f + 1.0f; |
| ASSERT_EQ(encoder_float_->encode(expected[i], out_stream), |
| common::E_OK); |
| } |
| ASSERT_EQ(encoder_float_->flush(out_stream), common::E_OK); |
| |
| std::vector<float> actual_values(row_num); |
| int actual = 0; |
| ASSERT_EQ(decoder_float_->read_batch_float(actual_values.data(), row_num, |
| actual, out_stream), |
| common::E_OK); |
| ASSERT_EQ(actual, row_num); |
| for (int i = 0; i < row_num; ++i) { |
| EXPECT_FLOAT_EQ(actual_values[i], expected[i]) << "row " << i; |
| } |
| EXPECT_FALSE(decoder_float_->has_remaining(out_stream)); |
| } |
| |
| TEST_F(FloatDoubleTS2DIFFCodecTest, ReadBatchDoubleConsumesOverflowPrefix) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| const double expected[] = {3.123456768E20, std::nan("")}; |
| for (double value : expected) { |
| ASSERT_EQ(encoder_double_->encode(value, out_stream), common::E_OK); |
| } |
| ASSERT_EQ(encoder_double_->flush(out_stream), common::E_OK); |
| |
| double actual_values[2] = {}; |
| int actual = 0; |
| ASSERT_EQ(decoder_double_->read_batch_double(actual_values, 2, actual, |
| out_stream), |
| common::E_OK); |
| ASSERT_EQ(actual, 2); |
| EXPECT_DOUBLE_EQ(actual_values[0], expected[0]); |
| EXPECT_TRUE(std::isnan(actual_values[1])); |
| EXPECT_FALSE(decoder_double_->has_remaining(out_stream)); |
| } |
| |
| TEST_F(TS2DIFFCodecTest, TestIntEncoding1) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| const int row_num = 10000; |
| int32_t data[row_num]; |
| memset(data, 0, sizeof(int32_t) * row_num); |
| for (int i = 0; i < row_num; i++) { |
| data[i] = i * i; |
| } |
| |
| for (int i = 0; i < row_num; i++) { |
| EXPECT_EQ(encoder_int_->encode(data[i], out_stream), common::E_OK); |
| } |
| EXPECT_EQ(encoder_int_->flush(out_stream), common::E_OK); |
| |
| int32_t x; |
| for (int i = 0; i < row_num; i++) { |
| EXPECT_EQ(decoder_int_->read_int32(x, out_stream), common::E_OK); |
| EXPECT_EQ(x, data[i]); |
| } |
| } |
| |
| TEST_F(TS2DIFFCodecTest, TestIntEncoding2) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| const int row_num = 10000; |
| int32_t data[row_num]; |
| memset(data, 0, sizeof(int32_t) * row_num); |
| for (int i = 0; i < row_num; i++) { |
| data[i] = i; |
| } |
| |
| for (int i = 0; i < row_num; i++) { |
| EXPECT_EQ(encoder_int_->encode(data[i], out_stream), common::E_OK); |
| } |
| EXPECT_EQ(encoder_int_->flush(out_stream), common::E_OK); |
| |
| int32_t x; |
| for (int i = 0; i < row_num; i++) { |
| EXPECT_EQ(decoder_int_->read_int32(x, out_stream), common::E_OK); |
| EXPECT_EQ(x, data[i]); |
| } |
| } |
| |
| TEST_F(TS2DIFFCodecTest, TestLongEncoding) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| const int row_num = 10000; |
| int64_t data[row_num]; |
| memset(data, 0, sizeof(int64_t) * row_num); |
| for (int i = 0; i < row_num; i++) { |
| data[i] = i; |
| } |
| |
| for (int i = 0; i < row_num; i++) { |
| EXPECT_EQ(encoder_long_->encode(data[i], out_stream), common::E_OK); |
| } |
| EXPECT_EQ(encoder_long_->flush(out_stream), common::E_OK); |
| |
| int64_t x; |
| for (int i = 0; i < row_num; i++) { |
| EXPECT_EQ(decoder_long_->read_int64(x, out_stream), common::E_OK); |
| EXPECT_EQ(x, data[i]); |
| } |
| } |
| |
| TEST_F(TS2DIFFCodecTest, TestLongEncoding2) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| const int row_num = 10000; |
| int64_t data[row_num]; |
| memset(data, 0, sizeof(int64_t) * row_num); |
| for (int i = 0; i < row_num; i++) { |
| data[i] = i * i; |
| } |
| |
| for (int i = 0; i < row_num; i++) { |
| EXPECT_EQ(encoder_long_->encode(data[i], out_stream), common::E_OK); |
| } |
| EXPECT_EQ(encoder_long_->flush(out_stream), common::E_OK); |
| |
| int64_t x; |
| for (int i = 0; i < row_num; i++) { |
| EXPECT_EQ(decoder_long_->read_int64(x, out_stream), common::E_OK); |
| EXPECT_EQ(x, data[i]); |
| } |
| } |
| |
| TEST_F(TS2DIFFCodecTest, TestRandomEncoding) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| const int row_num = 10000; |
| int64_t data[row_num]; |
| memset(data, 0, sizeof(int64_t) * row_num); |
| |
| std::mt19937 rng(std::random_device{}()); |
| int min = -100000; |
| int max = 100000; |
| std::uniform_int_distribution<int> dist(min, max); |
| for (int i = 0; i < row_num; i++) { |
| int random_number = dist(rng); |
| data[i] = random_number; |
| } |
| |
| for (int i = 0; i < row_num; i++) { |
| EXPECT_EQ(encoder_long_->encode(data[i], out_stream), common::E_OK); |
| } |
| EXPECT_EQ(encoder_long_->flush(out_stream), common::E_OK); |
| |
| int64_t x; |
| for (int i = 0; i < row_num; i++) { |
| EXPECT_EQ(decoder_long_->read_int64(x, out_stream), common::E_OK); |
| EXPECT_EQ(x, data[i]); |
| } |
| } |
| |
| TEST_F(TS2DIFFCodecTest, LargeDataTest) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| std::mt19937 gen(42); |
| std::uniform_int_distribution<int32_t> dist(-100000, 100000); |
| const int row_num = 2000000; |
| std::vector<int32_t> data(row_num); |
| for (int i = 0; i < row_num; i++) { |
| data[i] = dist(gen); |
| } |
| |
| auto start_encode = std::chrono::steady_clock::now(); |
| for (int i = 0; i < row_num; i++) { |
| EXPECT_EQ(encoder_int_->encode(data[i], out_stream), common::E_OK); |
| } |
| EXPECT_EQ(encoder_int_->flush(out_stream), common::E_OK); |
| auto end_encode = std::chrono::steady_clock::now(); |
| |
| std::vector<int32_t> decoded(row_num); |
| auto start_decode = std::chrono::steady_clock::now(); |
| for (int i = 0; i < row_num; i++) { |
| EXPECT_EQ(decoder_int_->read_int32(decoded[i], out_stream), |
| common::E_OK); |
| } |
| auto end_decode = std::chrono::steady_clock::now(); |
| |
| auto encode_duration = |
| std::chrono::duration_cast<std::chrono::milliseconds>(end_encode - |
| start_encode); |
| auto decode_duration = |
| std::chrono::duration_cast<std::chrono::milliseconds>(end_decode - |
| start_decode); |
| |
| std::cout << "Encode time: " << encode_duration.count() << "ms\n"; |
| std::cout << "Decode time: " << decode_duration.count() << "ms\n"; |
| } |
| |
| TEST_F(TS2DIFFCodecTest, TestEncodingLast) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| common::ByteStream out_stream_int32(1024, common::MOD_TS2DIFF_OBJ, false); |
| const int row_num = 6; |
| int64_t data[row_num]; |
| memset(data, 0, sizeof(int64_t) * row_num); |
| for (int i = 0; i < row_num; i++) { |
| data[i] = i * i; |
| } |
| |
| for (int i = 0; i < row_num; i++) { |
| EXPECT_EQ(encoder_long_->encode(data[i], out_stream), common::E_OK); |
| EXPECT_EQ(encoder_int_->encode((int32_t)data[i], out_stream_int32), |
| common::E_OK); |
| } |
| EXPECT_EQ(encoder_long_->flush(out_stream), common::E_OK); |
| EXPECT_EQ(encoder_int_->flush(out_stream_int32), common::E_OK); |
| |
| int64_t x; |
| int32_t y; |
| for (int i = 0; i < row_num; i++) { |
| EXPECT_EQ(decoder_long_->read_int64(x, out_stream), common::E_OK); |
| EXPECT_EQ(x, data[i]); |
| EXPECT_EQ(decoder_int_->read_int32(y, out_stream_int32), common::E_OK); |
| EXPECT_EQ(y, data[i]); |
| } |
| EXPECT_FALSE(decoder_long_->has_remaining(out_stream)); |
| EXPECT_FALSE(decoder_int_->has_remaining(out_stream_int32)); |
| } |
| |
| // Regression: skip_int32/skip_int64 used to advance the stream by the full |
| // block size even when the requested skip count fell short of the block, |
| // which silently dropped values from the next read in aligned nullable |
| // columns. Verify that skipping a count smaller than the first block leaves |
| // the remainder of that block intact and decodable. |
| TEST_F(TS2DIFFCodecTest, SkipPartialBlockInt32PreservesRemainder) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| const int row_num = 1024; |
| std::vector<int32_t> data(row_num); |
| for (int i = 0; i < row_num; i++) { |
| data[i] = i * 3 + 7; |
| } |
| for (int i = 0; i < row_num; i++) { |
| ASSERT_EQ(encoder_int_->encode(data[i], out_stream), common::E_OK); |
| } |
| ASSERT_EQ(encoder_int_->flush(out_stream), common::E_OK); |
| |
| const int skip_count = 5; |
| int skipped = 0; |
| ASSERT_EQ(decoder_int_->skip_int32(skip_count, skipped, out_stream), |
| common::E_OK); |
| EXPECT_EQ(skipped, skip_count); |
| |
| int32_t v; |
| for (int i = skip_count; i < row_num; i++) { |
| ASSERT_EQ(decoder_int_->read_int32(v, out_stream), common::E_OK); |
| EXPECT_EQ(v, data[i]) << "mismatch at idx " << i; |
| } |
| } |
| |
| TEST_F(TS2DIFFCodecTest, SkipPartialBlockInt64PreservesRemainder) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| const int row_num = 1024; |
| std::vector<int64_t> data(row_num); |
| for (int i = 0; i < row_num; i++) { |
| data[i] = static_cast<int64_t>(i) * 13 + 11; |
| } |
| for (int i = 0; i < row_num; i++) { |
| ASSERT_EQ(encoder_long_->encode(data[i], out_stream), common::E_OK); |
| } |
| ASSERT_EQ(encoder_long_->flush(out_stream), common::E_OK); |
| |
| const int skip_count = 7; |
| int skipped = 0; |
| ASSERT_EQ(decoder_long_->skip_int64(skip_count, skipped, out_stream), |
| common::E_OK); |
| EXPECT_EQ(skipped, skip_count); |
| |
| int64_t v; |
| for (int i = skip_count; i < row_num; i++) { |
| ASSERT_EQ(decoder_long_->read_int64(v, out_stream), common::E_OK); |
| EXPECT_EQ(v, data[i]) << "mismatch at idx " << i; |
| } |
| } |
| |
| // Regression: pack_bits_msb used to drop ByteStream::write_buf's return value |
| // on the floor and unconditionally return 0 (success). flush() then reported |
| // E_OK and reset() wiped encoder state even when the actual data never made |
| // it onto the stream. The fix surfaces the underlying error code via the |
| // helper's return value. |
| // |
| // We can't easily inject a real write failure without a custom allocator |
| // (ByteStream::write_buf only fails on OOM), so this test pins down the |
| // contract on the visible boundary: a wide bit_width must return the |
| // dedicated "fallback" sentinel (-1) so flush() knows to take the per-bit |
| // path, and the helper's return type must be the error code from write_buf |
| // otherwise. Future refactors that swallow the write error would either |
| // stop returning -1 for fallback (caught here) or break round-trip in the |
| // happy-path test below. |
| TEST_F(TS2DIFFCodecTest, PackBitsMsbFallbackSentinelStillReported) { |
| common::ByteStream out(1024, common::MOD_TS2DIFF_OBJ, false); |
| int64_t values[4] = {1, 2, 3, 4}; |
| EXPECT_EQ(TS2DIFFEncoder<int64_t>::pack_bits_msb(values, 4, 57, out), -1); |
| // Healthy small bit_width writes succeed. |
| int32_t small_values[4] = {1, 2, 3, 4}; |
| EXPECT_EQ(TS2DIFFEncoder<int32_t>::pack_bits_msb(small_values, 4, 3, out), |
| common::E_OK); |
| } |
| |
| // Regression: FloatTS2DIFFEncoder / DoubleTS2DIFFEncoder kept the previous |
| // page's overflow markers in underflow_flags_ when reset() was called |
| // directly (PageWriter drops a partial page that way). The next page would |
| // then read the stale flags and emit a wrong overflow bitmap. reset() now |
| // clears underflow_flags_; verify a reset between pages doesn't leak the |
| // first page's overflow state into the second. |
| TEST(FloatTS2DIFFEncoderResetTest, ResetClearsUnderflowFlags) { |
| storage::FloatTS2DIFFEncoder enc; |
| common::ByteStream out1(1024, common::MOD_TS2DIFF_OBJ, false); |
| // Encode a value that overflows the scale factor so the encoder records |
| // an underflow flag. |
| const float overflow_value = 1e30f; // scaled > INT32_MAX |
| ASSERT_EQ(enc.encode(0.0f, out1), common::E_OK); |
| ASSERT_EQ(enc.encode(overflow_value, out1), common::E_OK); |
| |
| // Drop the page without flushing. PageWriter does exactly this when |
| // discarding a half-built page. |
| enc.reset(); |
| |
| // Encode a clean page that should not have any overflow markers. |
| common::ByteStream out2(1024, common::MOD_TS2DIFF_OBJ, false); |
| ASSERT_EQ(enc.encode(0.0f, out2), common::E_OK); |
| ASSERT_EQ(enc.encode(1.0f, out2), common::E_OK); |
| ASSERT_EQ(enc.encode(2.0f, out2), common::E_OK); |
| ASSERT_EQ(enc.flush(out2), common::E_OK); |
| |
| // Round-trip the clean page; if reset() leaked the stale overflow flags |
| // the decoder would misinterpret the leading bytes as an overflow |
| // bitmap header and fail to recover the original values. |
| storage::FloatTS2DIFFDecoder dec; |
| float v = 0.0f; |
| for (int i = 0; i < 3; i++) { |
| ASSERT_EQ(dec.read_float(v, out2), common::E_OK); |
| EXPECT_NEAR(v, static_cast<float>(i), 1e-5f); |
| } |
| } |
| |
| // Regression: legacy raw float/double segments (written by the old C++ |
| // encoders, i.e. plain int delta blocks with no maxPointNumber / overflow |
| // prefix, values stored as bit-cast float bits) must stay decodable through |
| // Legacy raw TS_2DIFF pages (pre-#796 C++ writer output: plain int delta |
| // blocks over bit-cast float bits, no maxPointNumber / bitmap prefix) are |
| // outside the cross-language format: the Java reader never supported them. |
| // The decoder treats such input as a format error instead of guessing the |
| // layout: it must fail fast rather than hang at end-of-input or silently |
| // return misdecoded values. |
| // |
| // A raw block starts with the 4-byte big-endian write_index whose high |
| // byte is 0x00; to the page-metadata parser that is a valid |
| // maxPointNumber = 0 (Form 1), so detection happens at the block level: |
| // the following bytes are not a consistent block stream and the batch |
| // reader must terminate with an error rather than loop forever. |
| TEST_F(FloatDoubleTS2DIFFCodecTest, ReadBatchFloatLegacyRawSegments) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| const int row_num = 129; |
| std::vector<float> expected(row_num); |
| for (int i = 0; i < row_num; ++i) { |
| expected[i] = (i < 128) ? 1.5f : 2.5f; |
| } |
| IntTS2DIFFEncoder raw_encoder; |
| for (int i = 0; i < row_num; ++i) { |
| ASSERT_EQ( |
| raw_encoder.encode(common::float_to_int(expected[i]), out_stream), |
| common::E_OK); |
| } |
| ASSERT_EQ(raw_encoder.flush(out_stream), common::E_OK); |
| |
| std::vector<float> actual_values(row_num); |
| int actual = 0; |
| // Must terminate (no end-of-input spin) and report a format error; |
| // never return E_OK with garbage values. |
| const int rc = decoder_float_->read_batch_float( |
| actual_values.data(), row_num, actual, out_stream); |
| ASSERT_NE(rc, common::E_OK); |
| } |
| |
| TEST_F(FloatDoubleTS2DIFFCodecTest, ReadBatchDoubleLegacyRawSegments) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| const int row_num = 129; |
| std::vector<double> expected(row_num); |
| for (int i = 0; i < row_num; ++i) { |
| expected[i] = (i < 128) ? 1.5 : 2.5; |
| } |
| LongTS2DIFFEncoder raw_encoder; |
| for (int i = 0; i < row_num; ++i) { |
| ASSERT_EQ( |
| raw_encoder.encode(common::double_to_long(expected[i]), out_stream), |
| common::E_OK); |
| } |
| ASSERT_EQ(raw_encoder.flush(out_stream), common::E_OK); |
| |
| std::vector<double> actual_values(row_num); |
| int actual = 0; |
| const int rc = decoder_double_->read_batch_double( |
| actual_values.data(), row_num, actual, out_stream); |
| ASSERT_NE(rc, common::E_OK); |
| } |
| |
| TEST_F(FloatDoubleTS2DIFFCodecTest, ReadFloatLegacyRawScalar) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| const int row_num = 129; |
| std::vector<float> expected(row_num); |
| for (int i = 0; i < row_num; ++i) { |
| expected[i] = (i < 128) ? 1.5f : 2.5f; |
| } |
| IntTS2DIFFEncoder raw_encoder; |
| for (int i = 0; i < row_num; ++i) { |
| ASSERT_EQ( |
| raw_encoder.encode(common::float_to_int(expected[i]), out_stream), |
| common::E_OK); |
| } |
| ASSERT_EQ(raw_encoder.flush(out_stream), common::E_OK); |
| |
| float v = 0.f; |
| // Scalar path: may return a bounded number of values, but must not |
| // spin forever; assert that reading the full stream terminates and |
| // does not report success for all rows of a known-invalid layout. |
| int ok_rows = 0; |
| int rc = common::E_OK; |
| for (int i = 0; i < row_num; ++i) { |
| rc = decoder_float_->read_float(v, out_stream); |
| if (rc != common::E_OK) break; |
| ok_rows++; |
| } |
| // Termination is the contract; the values themselves are undefined |
| // for this out-of-format input. |
| SUCCEED(); |
| } |
| |
| TEST_F(FloatDoubleTS2DIFFCodecTest, ReadDoubleLegacyRawScalar) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| const int row_num = 129; |
| std::vector<double> expected(row_num); |
| for (int i = 0; i < row_num; ++i) { |
| expected[i] = (i < 128) ? 1.5 : 2.5; |
| } |
| LongTS2DIFFEncoder raw_encoder; |
| for (int i = 0; i < row_num; ++i) { |
| ASSERT_EQ( |
| raw_encoder.encode(common::double_to_long(expected[i]), out_stream), |
| common::E_OK); |
| } |
| ASSERT_EQ(raw_encoder.flush(out_stream), common::E_OK); |
| |
| double v = 0.; |
| int rc = common::E_OK; |
| for (int i = 0; i < row_num; ++i) { |
| rc = decoder_double_->read_double(v, out_stream); |
| if (rc != common::E_OK) break; |
| } |
| SUCCEED(); |
| } |
| |
| // Mixed reads on a legacy raw page: the batch reader must fail fast; the |
| // scalar reader after it must also terminate. |
| TEST_F(FloatDoubleTS2DIFFCodecTest, LegacyRawBatchThenScalarReads) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| const int row_num = 129; |
| std::vector<float> expected(row_num); |
| for (int i = 0; i < row_num; ++i) { |
| expected[i] = (i < 128) ? 1.0f : 100.0f; |
| } |
| IntTS2DIFFEncoder raw_encoder; |
| for (int i = 0; i < row_num; ++i) { |
| ASSERT_EQ( |
| raw_encoder.encode(common::float_to_int(expected[i]), out_stream), |
| common::E_OK); |
| } |
| ASSERT_EQ(raw_encoder.flush(out_stream), common::E_OK); |
| |
| float batch_out[40]; |
| int actual = 0; |
| const int rc = |
| decoder_float_->read_batch_float(batch_out, 40, actual, out_stream); |
| ASSERT_NE(rc, common::E_OK); |
| float v = 0.f; |
| for (int i = 0; i < row_num; ++i) { |
| if (decoder_float_->read_float(v, out_stream) != common::E_OK) break; |
| } |
| SUCCEED(); |
| } |
| // ============================================================================ |
| // apache/tsfile#910 regression: Java reads the maxPointNumber field only |
| // once per page (before the first segment); the old C++ encoder repeated it |
| // at every segment boundary, so multi-segment pages could be misparsed by |
| // Java readers (TsFileSketchTool / print-tsfile.bat crash). |
| // |
| // The fixed encoder emits maxPointNumber exactly once per page (a page |
| // boundary is signaled by reset()); later segments start directly with the |
| // 4-byte write_index. The decoder distinguishes the two layouts by peeking |
| // the byte at the segment start: 0x00 means "no prefix" (write_index high |
| // byte), any non-zero byte is a var_uint tag (maxPointNumber itself, or the |
| // overflow flag). |
| // ============================================================================ |
| |
| namespace { |
| |
| // LEB128 var_uint, matching common::SerializationUtil::write_var_uint. |
| bool parse_var_uint(const std::vector<uint8_t>& b, size_t& pos, uint32_t& out) { |
| if (pos >= b.size()) return false; |
| out = 0; |
| int shift = 0; |
| while (true) { |
| if (pos >= b.size() || shift > 28) return false; |
| uint8_t byte = b[pos++]; |
| out |= static_cast<uint32_t>(byte & 0x7F) << shift; |
| if ((byte & 0x80) == 0) return true; |
| shift += 7; |
| } |
| } |
| |
| int32_t read_i32_be(const std::vector<uint8_t>& b, size_t pos) { |
| return (static_cast<int32_t>(b[pos]) << 24) | |
| (static_cast<int32_t>(b[pos + 1]) << 16) | |
| (static_cast<int32_t>(b[pos + 2]) << 8) | |
| static_cast<int32_t>(b[pos + 3]); |
| } |
| |
| // Dumps the full stream content and CONSUMES the stream (read position |
| // moves to the end). Callers decode from a wrapped copy of the bytes: |
| // restoring the read position to a page-aligned offset (position 0) makes |
| // ByteStream::check_space() advance its page cursor one page too far and |
| // fail the next read, so no rewind is attempted here. |
| std::vector<uint8_t> byte_stream_bytes(common::ByteStream& stream) { |
| uint32_t size = stream.total_size(); |
| std::vector<uint8_t> buf(size); |
| uint32_t read_len = 0; |
| EXPECT_EQ(stream.read_buf(buf.data(), size, read_len), common::E_OK); |
| EXPECT_EQ(read_len, size); |
| return buf; |
| } |
| |
| // Wraps dumped page bytes for decoding (same path production chunk readers |
| // use — a wrapped ByteStream). |
| void wrap_bytes(const std::vector<uint8_t>& b, common::ByteStream& s) { |
| s.wrap_from(reinterpret_cast<const char*>(b.data()), |
| static_cast<int32_t>(b.size())); |
| } |
| |
| // Walks a float/double TS_2DIFF page and asserts the apache/tsfile#910 |
| // layout invariant: the maxPointNumber var_uint appears only on the page's |
| // first segment (possibly after a leading overflow-marker section); every |
| // later segment starts directly with its 4-byte write_index, so any |
| // non-flag tag on a later segment is a regression. Segments hold up to 129 |
| // values (write_index 128); the walker sanity-checks the header fields and |
| // skips the packed delta body. |
| // Structural page walker for the canonical Java layout: parses the page |
| // metadata once, then walks the integer block stream. Verifies that the |
| // metadata (and thus maxPointNumber) appears exactly once per page and |
| // that every block header is well formed. |
| void expect_max_pn_once_per_page(const std::vector<uint8_t>& b, |
| bool is_double) { |
| const uint32_t FLAG_SCALED = 2147483647u; |
| const uint32_t FLAG_ORIGINAL = 2147483646u; |
| size_t pos = 0; |
| size_t header_len = is_double ? 24 : 16; |
| |
| // Page metadata, exactly once at the start. |
| ASSERT_FALSE(b.empty()); |
| uint32_t tag = 0; |
| ASSERT_TRUE(parse_var_uint(b, pos, tag)); |
| if (tag == FLAG_SCALED || tag == FLAG_ORIGINAL) { |
| // Forms 2/3: [marker][count][bitmap(s)][maxPointNumber]. |
| uint32_t n = 0; |
| ASSERT_TRUE(parse_var_uint(b, pos, n)); |
| EXPECT_GE(n, 1u); |
| size_t bm_len = static_cast<size_t>(n / 8 + 1); |
| ASSERT_LE(pos + bm_len, b.size()); |
| pos += bm_len; |
| if (tag == FLAG_ORIGINAL) { |
| ASSERT_LE(pos + bm_len, b.size()); |
| pos += bm_len; |
| } |
| uint32_t mpn = 0; |
| ASSERT_TRUE(parse_var_uint(b, pos, mpn)); |
| EXPECT_EQ(mpn, 2u) << "default encoder writes maxPointNumber = 2"; |
| } else { |
| // Form 1: the leading varint IS the maxPointNumber (0x02 = 2). |
| EXPECT_EQ(tag, 2u) << "default encoder writes maxPointNumber = 2"; |
| } |
| |
| // Continuous integer block stream with no float metadata between |
| // blocks. |
| int segment_count = 0; |
| while (pos < b.size()) { |
| ASSERT_LE(pos + header_len, b.size()); |
| int32_t wi = read_i32_be(b, pos); |
| int32_t bw = read_i32_be(b, pos + 4); |
| ASSERT_GE(wi, 0) << "negative write_index at segment " |
| << segment_count + 1; |
| EXPECT_LE(wi, 128); |
| ASSERT_GE(bw, 0); |
| EXPECT_LE(bw, 64); |
| pos += header_len; |
| pos += (static_cast<size_t>(wi) * static_cast<size_t>(bw) + 7) / 8; |
| ASSERT_LE(pos, b.size()); |
| segment_count++; |
| } |
| EXPECT_GE(segment_count, 1); |
| } |
| |
| } // namespace |
| |
| // A page holds multiple 129-value segments; the maxPointNumber must appear |
| // exactly once, at the page start — not at every segment boundary. |
| TEST_F(FloatDoubleTS2DIFFCodecTest, |
| MaxPointNumberOncePerPageFloatMultiSegment) { |
| common::ByteStream out(1024, common::MOD_TS2DIFF_OBJ, false); |
| const int row_num = 400; // 4 segments: 129 + 129 + 129 + 13 |
| std::vector<float> data(row_num); |
| for (int i = 0; i < row_num; i++) { |
| data[i] = static_cast<float>(i) * 0.25f + 0.5f; |
| } |
| for (int i = 0; i < row_num; i++) { |
| ASSERT_EQ(encoder_float_->encode(data[i], out), common::E_OK); |
| } |
| ASSERT_EQ(encoder_float_->flush(out), common::E_OK); |
| |
| // The default maxPointNumber is 2 (TSFileConfig.floatPrecision), so |
| // the page starts with the single 0x02 mpn byte; the structural scan |
| // below verifies the prefix appears exactly once. |
| std::vector<uint8_t> b = byte_stream_bytes(out); |
| ASSERT_FALSE(b.empty()); |
| EXPECT_EQ(b[0], 0x02) << "page must start with the maxPointNumber=2 byte"; |
| expect_max_pn_once_per_page(b, false); |
| |
| common::ByteStream dec; |
| wrap_bytes(b, dec); |
| float x = 0.0f; |
| for (int i = 0; i < row_num; i++) { |
| ASSERT_EQ(decoder_float_->read_float(x, dec), common::E_OK); |
| EXPECT_FLOAT_EQ(x, data[i]) << "row " << i; |
| } |
| EXPECT_FALSE(decoder_float_->has_remaining(dec)); |
| } |
| |
| // Same invariant for the double encoder (i64 delta path). |
| TEST_F(FloatDoubleTS2DIFFCodecTest, |
| MaxPointNumberOncePerPageDoubleMultiSegment) { |
| common::ByteStream out(1024, common::MOD_TS2DIFF_OBJ, false); |
| const int row_num = 400; |
| std::vector<double> data(row_num); |
| for (int i = 0; i < row_num; i++) { |
| data[i] = static_cast<double>(i) * 0.25 + 0.5; |
| } |
| for (int i = 0; i < row_num; i++) { |
| ASSERT_EQ(encoder_double_->encode(data[i], out), common::E_OK); |
| } |
| ASSERT_EQ(encoder_double_->flush(out), common::E_OK); |
| |
| std::vector<uint8_t> b = byte_stream_bytes(out); |
| ASSERT_FALSE(b.empty()); |
| EXPECT_EQ(b[0], 0x02) << "page must start with the maxPointNumber=2 byte"; |
| expect_max_pn_once_per_page(b, true); |
| |
| common::ByteStream dec; |
| wrap_bytes(b, dec); |
| double y = 0.0; |
| for (int i = 0; i < row_num; i++) { |
| ASSERT_EQ(decoder_double_->read_double(y, dec), common::E_OK); |
| EXPECT_DOUBLE_EQ(y, data[i]) << "row " << i; |
| } |
| EXPECT_FALSE(decoder_double_->has_remaining(dec)); |
| } |
| |
| // The #910 crash scenario: a value that overflows the scaled range in the |
| // first segment. The overflow-marker section leads the page, the single |
| // maxPointNumber follows it, and the second segment still has no prefix. |
| TEST_F(FloatDoubleTS2DIFFCodecTest, |
| MaxPointNumberOncePerPageFloatWithOverflow) { |
| common::ByteStream out(1024, common::MOD_TS2DIFF_OBJ, false); |
| const int row_num = 140; // segment 1 (129 values) + segment 2 (11 values) |
| std::vector<float> data(row_num); |
| data[0] = 0.5f; |
| data[1] = 3.0e7f; // *100 = 3e9 > INT32_MAX → scaled overflow (flag 0) |
| for (int i = 2; i < row_num; i++) { |
| data[i] = 0.75f + static_cast<float>(i - 2) * 0.25f; |
| } |
| for (int i = 0; i < row_num; i++) { |
| ASSERT_EQ(encoder_float_->encode(data[i], out), common::E_OK); |
| } |
| ASSERT_EQ(encoder_float_->flush(out), common::E_OK); |
| |
| // Byte layout: [FLAG var_uint][pageValueCount=140][page-wide |
| // underflow bitmap (18B)][maxPointNumber 0x02][block 1 header][packed] |
| // [block 2 header starting with 0x00 — no prefix] |
| std::vector<uint8_t> b = byte_stream_bytes(out); |
| size_t pos = 0; |
| uint32_t tag = 0; |
| ASSERT_TRUE(parse_var_uint(b, pos, tag)); |
| EXPECT_EQ(tag, ts2diff_java_detail::FLAG_SCALED_VALUE_OVERFLOW); |
| uint32_t n = 0; |
| ASSERT_TRUE(parse_var_uint(b, pos, n)); |
| EXPECT_EQ(n, 140u); // page-wide bitmap covers every value in the page |
| size_t bm_len = static_cast<size_t>(n / 8 + 1); |
| ASSERT_LE(pos + bm_len, b.size()); |
| pos += bm_len; |
| // Exactly one maxPointNumber, directly after the bitmap. |
| ASSERT_LT(pos, b.size()); |
| EXPECT_EQ(b[pos], 0x02) << "maxPointNumber = 2 byte"; |
| uint32_t mpn = 0; |
| ASSERT_TRUE(parse_var_uint(b, pos, mpn)); |
| EXPECT_EQ(mpn, 2u); |
| // Segment 1 header: write_index == 128 (129 values). |
| ASSERT_LE(pos + 16, b.size()); |
| int32_t wi = read_i32_be(b, pos); |
| int32_t bw = read_i32_be(b, pos + 4); |
| EXPECT_EQ(wi, 128); |
| pos += 16; |
| pos += (static_cast<size_t>(wi) * static_cast<size_t>(bw) + 7) / 8; |
| ASSERT_LE(pos, b.size()); |
| // Segment 2 begins directly with its write_index (0x00 high byte). |
| EXPECT_EQ(b[pos], 0x00) << "segment 2 must not carry a maxPointNumber"; |
| |
| // Round-trip: the overflow value goes through the bitmap path. |
| common::ByteStream dec; |
| wrap_bytes(b, dec); |
| float x = 0.0f; |
| for (int i = 0; i < row_num; i++) { |
| ASSERT_EQ(decoder_float_->read_float(x, dec), common::E_OK); |
| EXPECT_FLOAT_EQ(x, data[i]) << "row " << i; |
| } |
| EXPECT_FALSE(decoder_float_->has_remaining(dec)); |
| } |
| |
| // Same overflow layout for double (scaled > INT64_MAX → 1.0e17 * 100). |
| // The generic walker understands the FLAG + maxPointNumber + segments |
| // structure; round-trip goes through the bitmap path. |
| TEST_F(FloatDoubleTS2DIFFCodecTest, |
| MaxPointNumberOncePerPageDoubleWithOverflow) { |
| common::ByteStream out(1024, common::MOD_TS2DIFF_OBJ, false); |
| const int row_num = 140; |
| std::vector<double> data(row_num); |
| data[0] = 0.5; |
| data[1] = 1.0e17; // *100 = 1e19 > INT64_MAX → scaled overflow (flag 0) |
| for (int i = 2; i < row_num; i++) { |
| data[i] = 0.75 + static_cast<double>(i - 2) * 0.25; |
| } |
| for (int i = 0; i < row_num; i++) { |
| ASSERT_EQ(encoder_double_->encode(data[i], out), common::E_OK); |
| } |
| ASSERT_EQ(encoder_double_->flush(out), common::E_OK); |
| |
| std::vector<uint8_t> b = byte_stream_bytes(out); |
| expect_max_pn_once_per_page(b, true); |
| |
| common::ByteStream dec; |
| wrap_bytes(b, dec); |
| double y = 0.0; |
| for (int i = 0; i < row_num; i++) { |
| ASSERT_EQ(decoder_double_->read_double(y, dec), common::E_OK); |
| EXPECT_DOUBLE_EQ(y, data[i]) << "row " << i; |
| } |
| EXPECT_FALSE(decoder_double_->has_remaining(dec)); |
| } |
| |
| // PageWriter resets the encoder between pages; every page must carry its |
| // own maxPointNumber prefix (exactly one per page). |
| TEST_F(FloatDoubleTS2DIFFCodecTest, MaxPointNumberPerPageAfterReset) { |
| const int row_num = 130; // 129 + 1 → two segments per page |
| std::vector<float> data(row_num); |
| for (int i = 0; i < row_num; i++) { |
| data[i] = static_cast<float>(i) * 0.25f + 0.5f; |
| } |
| common::ByteStream page1(1024, common::MOD_TS2DIFF_OBJ, false); |
| common::ByteStream page2(1024, common::MOD_TS2DIFF_OBJ, false); |
| for (int i = 0; i < row_num; i++) { |
| ASSERT_EQ(encoder_float_->encode(data[i], page1), common::E_OK); |
| } |
| ASSERT_EQ(encoder_float_->flush(page1), common::E_OK); |
| encoder_float_->reset(); // page boundary |
| for (int i = 0; i < row_num; i++) { |
| ASSERT_EQ(encoder_float_->encode(data[i], page2), common::E_OK); |
| } |
| ASSERT_EQ(encoder_float_->flush(page2), common::E_OK); |
| |
| std::vector<uint8_t> b1 = byte_stream_bytes(page1); |
| std::vector<uint8_t> b2 = byte_stream_bytes(page2); |
| ASSERT_FALSE(b1.empty()); |
| ASSERT_FALSE(b2.empty()); |
| EXPECT_EQ(b1[0], 0x02) << "page 1 must start with maxPointNumber=2"; |
| EXPECT_EQ(b2[0], 0x02) << "page 2 must start with maxPointNumber=2"; |
| expect_max_pn_once_per_page(b1, false); |
| expect_max_pn_once_per_page(b2, false); |
| |
| // Both pages decode with the same decoder; PageReader calls reset() |
| // between pages, which must re-arm the per-page prefix state. |
| common::ByteStream d1; |
| common::ByteStream d2; |
| wrap_bytes(b1, d1); |
| wrap_bytes(b2, d2); |
| float x = 0.0f; |
| for (int i = 0; i < row_num; i++) { |
| ASSERT_EQ(decoder_float_->read_float(x, d1), common::E_OK); |
| EXPECT_FLOAT_EQ(x, data[i]) << "page1 row " << i; |
| } |
| EXPECT_FALSE(decoder_float_->has_remaining(d1)); |
| decoder_float_->reset(); // page boundary |
| for (int i = 0; i < row_num; i++) { |
| ASSERT_EQ(decoder_float_->read_float(x, d2), common::E_OK); |
| EXPECT_FLOAT_EQ(x, data[i]) << "page2 row " << i; |
| } |
| EXPECT_FALSE(decoder_float_->has_remaining(d2)); |
| } |
| |
| // Explicit maxPointNumber = 0 (an explicit max_point_number property on |
| // the Java side): a canonical Form 1 page starting with the 0x00 byte. |
| // At mpn = 0 every finite in-range value takes the scaled form, so the |
| // page has no bitmap; the 0x00 byte is the maxPointNumber varint itself, |
| // not a legacy marker. |
| TEST_F(FloatDoubleTS2DIFFCodecTest, MaxPointNumberZeroPagePrefix) { |
| const int row_num = 140; // 129 + 11: crosses the block boundary |
| std::vector<float> data(row_num); |
| for (int i = 0; i < row_num; i++) { |
| data[i] = static_cast<float>(i) * 2.0f + 1.0f; |
| } |
| common::ByteStream out(1024, common::MOD_TS2DIFF_OBJ, false); |
| encoder_float_->set_max_point_number(0); |
| for (int i = 0; i < row_num; i++) { |
| ASSERT_EQ(encoder_float_->encode(data[i], out), common::E_OK); |
| } |
| ASSERT_EQ(encoder_float_->flush(out), common::E_OK); |
| |
| std::vector<uint8_t> b = byte_stream_bytes(out); |
| ASSERT_FALSE(b.empty()); |
| EXPECT_EQ(b[0], 0x00) << "mpn=0 page must start with the 0x00 byte"; |
| // Form 1: the 0x00 byte is consumed as maxPointNumber, and the rest |
| // must be a well-formed block stream (the walker asserts mpn == 0). |
| size_t pos = 0; |
| uint32_t mpn = 999; |
| ASSERT_TRUE(parse_var_uint(b, pos, mpn)); |
| EXPECT_EQ(mpn, 0u); |
| |
| common::ByteStream dec; |
| wrap_bytes(b, dec); |
| float x = 0.0f; |
| for (int i = 0; i < row_num; i++) { |
| ASSERT_EQ(decoder_float_->read_float(x, dec), common::E_OK); |
| EXPECT_FLOAT_EQ(x, data[i]) << "row " << i; |
| } |
| EXPECT_FALSE(decoder_float_->has_remaining(dec)); |
| |
| // Same for double. |
| std::vector<double> data_d(row_num); |
| for (int i = 0; i < row_num; i++) { |
| data_d[i] = static_cast<double>(i) * 2.0 + 1.0; |
| } |
| common::ByteStream out_d(1024, common::MOD_TS2DIFF_OBJ, false); |
| encoder_double_->set_max_point_number(0); |
| for (int i = 0; i < row_num; i++) { |
| ASSERT_EQ(encoder_double_->encode(data_d[i], out_d), common::E_OK); |
| } |
| ASSERT_EQ(encoder_double_->flush(out_d), common::E_OK); |
| |
| std::vector<uint8_t> bd = byte_stream_bytes(out_d); |
| ASSERT_FALSE(bd.empty()); |
| EXPECT_EQ(bd[0], 0x00) << "mpn=0 page must start with the 0x00 byte"; |
| common::ByteStream dec_d; |
| wrap_bytes(bd, dec_d); |
| double y = 0.0; |
| for (int i = 0; i < row_num; i++) { |
| ASSERT_EQ(decoder_double_->read_double(y, dec_d), common::E_OK); |
| EXPECT_DOUBLE_EQ(y, data_d[i]) << "row " << i; |
| } |
| EXPECT_FALSE(decoder_double_->has_remaining(dec_d)); |
| } |
| |
| // The pre-#910 C++ encoder repeated the maxPointNumber at every segment |
| // boundary. That layout was never produced by the Java writer and is now |
| // outside the compatibility boundary: the decoder parses page metadata |
| // exactly once, so a hand-built old-format page fails the block-header |
| // validation instead of being silently misdecoded. |
| TEST_F(FloatDoubleTS2DIFFCodecTest, LegacyPerSegmentMaxPNRejected) { |
| // Build an old-format page by hand: 0x02 prefix before BOTH segments. |
| const std::vector<float> expected = {0.5f, 0.75f, 1.0f, 1.25f, |
| 1.5f, 1.75f, 2.0f, 2.25f}; |
| common::ByteStream old_fmt(1024, common::MOD_TS2DIFF_OBJ, false); |
| // Segment 1: 6 values (first 50, five deltas of 25), bit_width 0. |
| ASSERT_EQ(common::SerializationUtil::write_var_uint(2, old_fmt), |
| common::E_OK); |
| ASSERT_EQ(common::SerializationUtil::write_ui32(5, old_fmt), common::E_OK); |
| ASSERT_EQ(common::SerializationUtil::write_ui32(0, old_fmt), common::E_OK); |
| ASSERT_EQ(common::SerializationUtil::write_ui32(25, old_fmt), common::E_OK); |
| ASSERT_EQ(common::SerializationUtil::write_ui32(50, old_fmt), common::E_OK); |
| // Segment 2: 2 values (first 200, one delta of 25) — WITH prefix again. |
| ASSERT_EQ(common::SerializationUtil::write_var_uint(2, old_fmt), |
| common::E_OK); |
| ASSERT_EQ(common::SerializationUtil::write_ui32(1, old_fmt), common::E_OK); |
| ASSERT_EQ(common::SerializationUtil::write_ui32(0, old_fmt), common::E_OK); |
| ASSERT_EQ(common::SerializationUtil::write_ui32(25, old_fmt), common::E_OK); |
| ASSERT_EQ(common::SerializationUtil::write_ui32(200, old_fmt), |
| common::E_OK); |
| |
| // Segment 1 (a valid single-block Form 1 page) decodes. The trailing |
| // 0x02 of the second segment prefix is then read as a block header and |
| // fails validation, so the decode loop terminates (no end-of-input |
| // spin). The invariant is that the out-of-format continuation is |
| // never handed back as valid data: reading stops before the segment-2 |
| // values, either by returning an error or by yielding a mismatch. |
| float x = 0.0f; |
| size_t good = 0; |
| for (size_t i = 0; i < expected.size(); i++) { |
| if (decoder_float_->read_float(x, old_fmt) != common::E_OK) break; |
| if (std::fabs(x - expected[i]) > 1e-6f) break; |
| good++; |
| } |
| EXPECT_LT(good, expected.size()) |
| << "out-of-format continuation must not decode to the expected values"; |
| EXPECT_GE(good, 1u) << "segment 1 is a valid page and must still decode"; |
| } |
| |
| // A truncated page whose block header passes the availability check (the |
| // check grants slack for the delta_min/first_value fields) but whose |
| // packed bits never arrive: the scalar read path must terminate instead |
| // of spinning on a stale buffer. |
| TEST_F(FloatDoubleTS2DIFFCodecTest, ScalarReadTerminatesOnTruncatedBlock) { |
| // [mpn=2][wi=1][bw=8][dm=0][fv=42] - after the header, remaining=8 |
| // covers the 1 packed byte, then dm+fv consume it all. |
| const unsigned char page[] = { |
| 0x02, 0x00, 0x00, 0x00, 0x01, // write_index = 1 |
| 0x00, 0x00, 0x00, 0x08, // bit_width = 8 |
| 0x00, 0x00, 0x00, 0x00, // delta_min |
| 0x00, 0x00, 0x00, 0x2a, // first_value |
| }; |
| common::ByteStream dec; |
| dec.wrap_from(reinterpret_cast<const char*>(page), sizeof(page)); |
| FloatTS2DIFFDecoder decoder; |
| float x = 0.0f; |
| ASSERT_EQ(decoder.read_float(x, dec), common::E_OK); |
| EXPECT_FLOAT_EQ(x, 0.42f); // first_value 42 / maxPointValue 100 |
| // The stream is exhausted mid-block; further reads must return |
| // (garbage is acceptable - the caller stops on has_remaining) but |
| // never hang. |
| for (int i = 0; i < 3; i++) { |
| decoder.read_float(x, dec); |
| } |
| EXPECT_FALSE(decoder.has_remaining(dec)); |
| SUCCEED(); |
| } |
| |
| TEST_F(FloatDoubleTS2DIFFCodecTest, RejectsBlockBeyondPageValueCount) { |
| // [overflow marker][count=1][bitmap][mpn=0][wi=INT_MAX][bw=0]... |
| // A zero-width block has no packed bytes, so availability alone cannot |
| // bound wi; the page value count must provide the bound. |
| const unsigned char page[] = { |
| 0xff, 0xff, 0xff, 0xff, 0x07, 0x01, 0x00, 0x00, 0x7f, |
| 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, |
| }; |
| common::ByteStream dec; |
| dec.wrap_from(reinterpret_cast<const char*>(page), sizeof(page)); |
| FloatTS2DIFFDecoder decoder; |
| float value = 0.0f; |
| // A block header that violates the format is a decode error, matching |
| // the RLE/Sprintz/RLBE convention. |
| EXPECT_EQ(decoder.read_float(value, dec), common::E_DECODE_ERR); |
| } |
| |
| TEST_F(FloatDoubleTS2DIFFCodecTest, ScalarReadRejectsTruncatedFixedFields) { |
| // The page metadata is valid, but the block is missing one byte of its |
| // fixed first-value field. |
| const unsigned char page[] = { |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| }; |
| common::ByteStream dec; |
| dec.wrap_from(reinterpret_cast<const char*>(page), sizeof(page)); |
| FloatTS2DIFFDecoder decoder; |
| float value = 0.0f; |
| // A short read propagates the stream's own error rather than being |
| // rebranded as a format violation. |
| EXPECT_EQ(decoder.read_float(value, dec), common::E_PARTIAL_READ); |
| } |
| |
| // A page whose trailing block header is truncated mid-field: the skip |
| // paths must reject it via the grouped header-read check instead of |
| // acting on stale stack values. |
| TEST_F(TS2DIFFCodecTest, SkipRejectsTruncatedBlockHeader) { |
| // [wi=1][bw=8] then only 4 of the 8 dm/fv bytes. |
| const unsigned char page[] = { |
| 0x00, 0x00, 0x00, 0x01, // write_index = 1 |
| 0x00, 0x00, 0x00, 0x08, // bit_width = 8 |
| 0x00, 0x00, 0x00, 0x00, // delta_min |
| 0x00, 0x00, 0x00, // first_value truncated to 3 bytes |
| }; |
| common::ByteStream dec; |
| dec.wrap_from(reinterpret_cast<const char*>(page), sizeof(page)); |
| IntTS2DIFFDecoder decoder; |
| int skipped = 0; |
| const int rc = decoder.skip_int32(10, skipped, dec); |
| // Truncated header: the underlying short-read error propagates. |
| EXPECT_EQ(rc, common::E_PARTIAL_READ); |
| } |
| |
| // Java Math.round semantics: floor(x + 0.5), ties towards +infinity. |
| // -0.125 * 100 = -12.5 exactly (0.125 is a binary fraction), and the tie |
| // must store as -12 (std::lround would give -13), so the round trip |
| // returns -0.12 rather than -0.13. |
| TEST_F(FloatDoubleTS2DIFFCodecTest, JavaRoundNegativeHalfTiesFloat) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| const float data[] = {-0.125f, -0.375f, 0.125f, 0.375f}; |
| const float expect[] = {-0.12f, -0.37f, 0.13f, 0.38f}; |
| for (float v : data) { |
| ASSERT_EQ(encoder_float_->encode(v, out_stream), common::E_OK); |
| } |
| ASSERT_EQ(encoder_float_->flush(out_stream), common::E_OK); |
| |
| float x = 0.0f; |
| for (int i = 0; i < 4; i++) { |
| ASSERT_EQ(decoder_float_->read_float(x, out_stream), common::E_OK); |
| EXPECT_FLOAT_EQ(x, expect[i]) << "value " << i; |
| } |
| } |
| |
| TEST_F(FloatDoubleTS2DIFFCodecTest, JavaRoundNegativeHalfTiesDouble) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| const double data[] = {-2.5, -7.5, 2.5, 7.5}; // mpn=0: scaled == value |
| const double expect[] = {-2.0, -7.0, 3.0, 8.0}; |
| encoder_double_->set_max_point_number(0); |
| for (double v : data) { |
| ASSERT_EQ(encoder_double_->encode(v, out_stream), common::E_OK); |
| } |
| ASSERT_EQ(encoder_double_->flush(out_stream), common::E_OK); |
| |
| double y = 0.0; |
| for (int i = 0; i < 4; i++) { |
| ASSERT_EQ(decoder_double_->read_double(y, out_stream), common::E_OK); |
| EXPECT_DOUBLE_EQ(y, expect[i]) << "value " << i; |
| } |
| } |
| |
| // writeIndex is bounded by availability, not BLOCK_DEFAULT_SIZE: a |
| // hand-built block larger than 129 values (Java exposes block-size |
| // constructors) must decode through both the scalar and batch paths. |
| TEST_F(TS2DIFFCodecTest, LargeBlockBeyondDefaultSizeDecodes) { |
| const int kCount = 200; // wi = 199 > 128 |
| const int32_t first = 1000; |
| const int32_t delta = 3; |
| const int32_t bit_width = 3; |
| |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| ASSERT_EQ(common::SerializationUtil::write_ui32(kCount - 1, out_stream), |
| common::E_OK); |
| ASSERT_EQ(common::SerializationUtil::write_ui32(bit_width, out_stream), |
| common::E_OK); |
| // delta_min: constant deltas of 3 rebase to 0 (raw - min), so the |
| // decoder reconstructs value += stored(0) + delta_min(3) per step. |
| ASSERT_EQ(common::SerializationUtil::write_ui32(delta, out_stream), |
| common::E_OK); |
| ASSERT_EQ(common::SerializationUtil::write_ui32(first, out_stream), |
| common::E_OK); |
| // 199 * 3 bits, MSB-packed, all-zero bytes == rebased delta 0. The |
| // packed body is ceil(bits / 8) = 75 bytes. |
| const int packed_bytes = ((kCount - 1) * bit_width + 7) / 8; // 75 |
| for (int i = 0; i < packed_bytes; i++) { |
| ASSERT_EQ(common::SerializationUtil::write_ui8(0, out_stream), |
| common::E_OK); |
| } |
| |
| // Copy the encoded page into a plain buffer; wrap two read streams |
| // over it (get_wrapped_buf is only valid for wrap_from streams). |
| std::vector<uint8_t> page(out_stream.total_size()); |
| uint32_t read_len = 0; |
| ASSERT_EQ( |
| out_stream.read_buf(page.data(), out_stream.total_size(), read_len), |
| common::E_OK); |
| ASSERT_EQ(read_len, page.size()); |
| |
| // Scalar path. |
| common::ByteStream dec1; |
| dec1.wrap_from(reinterpret_cast<const char*>(page.data()), |
| static_cast<int32_t>(page.size())); |
| IntTS2DIFFDecoder dec_scalar; |
| int32_t v = 0; |
| ASSERT_EQ(dec_scalar.read_int32(v, dec1), common::E_OK); |
| EXPECT_EQ(v, first); |
| for (int i = 1; i < kCount; i++) { |
| ASSERT_EQ(dec_scalar.read_int32(v, dec1), common::E_OK); |
| EXPECT_EQ(v, first + i * delta) << "row " << i; |
| } |
| EXPECT_FALSE(dec_scalar.has_remaining(dec1)); |
| |
| // Batch path. |
| common::ByteStream dec2; |
| dec2.wrap_from(reinterpret_cast<const char*>(page.data()), |
| static_cast<int32_t>(page.size())); |
| IntTS2DIFFDecoder dec_batch; |
| std::vector<int32_t> out(kCount); |
| int actual = 0; |
| ASSERT_EQ(dec_batch.read_batch_int32(out.data(), kCount, actual, dec2), |
| common::E_OK); |
| ASSERT_EQ(actual, kCount); |
| for (int i = 0; i < kCount; i++) { |
| EXPECT_EQ(out[i], first + i * delta) << "row " << i; |
| } |
| } |
| |
| // maxPointNumber has no format-level upper bound: a page written with |
| // mpn = 1000 (Java Math.pow overflows to +inf for the decoder, values |
| // then divide by inf) stays readable; the decoder must accept the varint |
| // instead of rejecting it as corrupt. |
| TEST_F(FloatDoubleTS2DIFFCodecTest, MaxPointNumberAboveLegacyBoundDecodes) { |
| common::ByteStream out_stream(1024, common::MOD_TS2DIFF_OBJ, false); |
| encoder_float_->set_max_point_number(1000); |
| const float data[] = {1.0f, 2.0f, 3.0f}; |
| for (float v : data) { |
| ASSERT_EQ(encoder_float_->encode(v, out_stream), common::E_OK); |
| } |
| ASSERT_EQ(encoder_float_->flush(out_stream), common::E_OK); |
| |
| // The encoder stores round(v * 10^1000); the double product overflows |
| // to +inf for every value, so every entry takes the raw-bits form |
| // (Form 3). The decode restores the exact bits. |
| std::vector<uint8_t> page(out_stream.total_size()); |
| uint32_t read_len = 0; |
| ASSERT_EQ( |
| out_stream.read_buf(page.data(), out_stream.total_size(), read_len), |
| common::E_OK); |
| ASSERT_EQ(read_len, page.size()); |
| common::ByteStream dec; |
| dec.wrap_from(reinterpret_cast<const char*>(page.data()), |
| static_cast<int32_t>(page.size())); |
| float x = 0.0f; |
| for (int i = 0; i < 3; i++) { |
| ASSERT_EQ(decoder_float_->read_float(x, dec), common::E_OK); |
| EXPECT_EQ(common::float_to_int(x), common::float_to_int(data[i])) |
| << "row " << i; |
| } |
| } |
| |
| } // namespace storage |