blob: 62d5167f36eef491655a721d7e4ae25b73792af0 [file]
/*
* 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 "writer/tsfile_writer.h"
#include <gtest/gtest.h>
#include <cstring>
#include <fstream>
#include <random>
#include "common/path.h"
#include "common/record.h"
#include "common/schema.h"
#include "common/tablet.h"
#include "common/tsfile_common.h"
#include "file/tsfile_io_writer.h"
#include "file/write_file.h"
#include "reader/qds_without_timegenerator.h"
#include "reader/tsfile_reader.h"
#include "writer/chunk_writer.h"
using namespace storage;
using namespace common;
class TsFileWriterTest : public ::testing::Test {
protected:
void SetUp() override {
libtsfile_init();
tsfile_writer_ = new TsFileWriter();
file_name_ = std::string("tsfile_writer_test_") +
generate_random_string(10) + std::string(".tsfile");
remove(file_name_.c_str());
int flags = O_WRONLY | O_CREAT | O_TRUNC;
#ifdef _WIN32
flags |= O_BINARY;
#endif
mode_t mode = 0666;
ASSERT_EQ(tsfile_writer_->open(file_name_, flags, mode), common::E_OK);
}
void TearDown() override {
delete tsfile_writer_;
int ret = remove(file_name_.c_str());
ASSERT_EQ(0, ret);
libtsfile_destroy();
}
std::string file_name_;
TsFileWriter* tsfile_writer_ = nullptr;
public:
static std::string generate_random_string(int length) {
std::mt19937 gen(static_cast<unsigned int>(
std::chrono::system_clock::now().time_since_epoch().count()));
std::uniform_int_distribution<> dis(0, 61);
const std::string chars =
"0123456789"
"abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
std::string random_string;
for (int i = 0; i < length; ++i) {
random_string += chars[dis(gen)];
}
return random_string;
}
static std::string field_to_string(storage::Field* value) {
if (value->type_ == common::TEXT || value->type_ == STRING ||
value->type_ == BLOB) {
return std::string(value->value_.sval_);
} else {
std::stringstream ss;
switch (value->type_) {
case common::BOOLEAN:
ss << (value->value_.bval_ ? "true" : "false");
break;
case common::INT32:
ss << value->value_.ival_;
break;
case common::INT64:
case common::TIMESTAMP:
ss << value->value_.lval_;
break;
case common::FLOAT:
ss << value->value_.fval_;
break;
case common::DOUBLE:
ss << value->value_.dval_;
break;
case common::NULL_TYPE:
ss << "NULL";
break;
default:
ASSERT(false);
break;
}
return ss.str();
}
}
};
class TsFileWriterTestSimple : public ::testing::Test {};
TEST_F(TsFileWriterTest, WriteDiffDataType) {
std::string device_name = "test_table";
common::TSEncoding encoding = common::TSEncoding::PLAIN;
common::CompressionType compression_type =
common::CompressionType::UNCOMPRESSED;
std::vector<std::string> measurement_names = {
"level", "num", "bools", "double", "id", "ts", "text", "blob", "date"};
std::vector<common::TSDataType> data_types = {
FLOAT, INT64, BOOLEAN, DOUBLE, STRING, TIMESTAMP, TEXT, BLOB, DATE};
for (uint32_t i = 0; i < measurement_names.size(); i++) {
std::string measurement_name = measurement_names[i];
common::TSDataType data_type = data_types[i];
tsfile_writer_->register_timeseries(
device_name,
storage::MeasurementSchema(measurement_name, data_type, encoding,
compression_type));
}
char* literal = new char[std::strlen("device_id") + 1];
std::strcpy(literal, "device_id");
String literal_str(literal, std::strlen("device_id"));
std::time_t now = std::time(nullptr);
std::tm* local_time = std::localtime(&now);
std::tm today = {};
today.tm_year = local_time->tm_year;
today.tm_mon = local_time->tm_mon;
today.tm_mday = local_time->tm_mday;
int row_num = 100000;
for (int i = 0; i < row_num; ++i) {
TsRecord record(1622505600000 + i * 100, device_name);
for (uint32_t j = 0; j < measurement_names.size(); j++) {
std::string measurement_name = measurement_names[j];
common::TSDataType data_type = data_types[j];
switch (data_type) {
case BOOLEAN:
record.add_point(measurement_name, true);
break;
case INT64:
record.add_point(measurement_name, (int64_t)415412);
break;
case FLOAT:
record.add_point(measurement_name, (float)1.0);
break;
case DOUBLE:
record.add_point(measurement_name, (double)2.0);
break;
case STRING:
record.add_point(measurement_name, literal_str);
break;
case TEXT:
record.add_point(measurement_name, literal_str);
break;
case BLOB:
record.add_point(measurement_name, literal_str);
break;
case TIMESTAMP:
record.add_point(measurement_name, (int64_t)415412);
break;
case DATE:
record.add_point(measurement_name, today);
default:
break;
}
}
ASSERT_EQ(tsfile_writer_->write_record(record), E_OK);
}
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
ASSERT_EQ(tsfile_writer_->close(), E_OK);
std::vector<std::string> select_list;
select_list.reserve(measurement_names.size());
for (uint32_t i = 0; i < measurement_names.size(); ++i) {
std::string measurement_name = measurement_names[i];
std::string path_name = device_name + "." + measurement_name;
select_list.emplace_back(path_name);
}
storage::TsFileReader reader;
int ret = reader.open(file_name_);
ASSERT_EQ(ret, common::E_OK);
storage::ResultSet* tmp_qds = nullptr;
ret = reader.query(select_list, 1622505600000,
1622505600000 + row_num * 100, tmp_qds);
auto* qds = (QDSWithoutTimeGenerator*)tmp_qds;
int64_t cur_record_num = 0;
bool has_next = false;
do {
if (IS_FAIL(qds->next(has_next)) || !has_next) {
break;
}
cur_record_num++;
ASSERT_EQ(qds->get_value<float>(2), (float)1.0);
ASSERT_EQ(qds->get_value<int64_t>(3), (int64_t)415412);
ASSERT_EQ(qds->get_value<bool>(4), true);
ASSERT_EQ(qds->get_value<double>(5), (double)2.0);
ASSERT_EQ(qds->get_value<common::String*>(6)->compare(literal_str), 0);
ASSERT_EQ(qds->get_value<int64_t>(7), (int64_t)415412);
ASSERT_EQ(qds->get_value<common::String*>(8)->compare(literal_str), 0);
ASSERT_EQ(qds->get_value<common::String*>(9)->compare(literal_str), 0);
ASSERT_TRUE(
DateConverter::is_tm_ymd_equal(qds->get_value<std::tm>(10), today));
ASSERT_EQ(qds->get_value<float>(measurement_names[0]), (float)1.0);
ASSERT_EQ(qds->get_value<int64_t>(measurement_names[1]),
(int64_t)415412);
ASSERT_EQ(qds->get_value<bool>(measurement_names[2]), true);
ASSERT_EQ(qds->get_value<double>(measurement_names[3]), (double)2.0);
ASSERT_EQ(qds->get_value<common::String*>(measurement_names[4])
->compare(literal_str),
0);
ASSERT_EQ(qds->get_value<int64_t>(measurement_names[5]),
(int64_t)415412);
ASSERT_EQ(qds->get_value<common::String*>(measurement_names[6])
->compare(literal_str),
0);
ASSERT_EQ(qds->get_value<common::String*>(measurement_names[7])
->compare(literal_str),
0);
ASSERT_TRUE(DateConverter::is_tm_ymd_equal(
qds->get_value<std::tm>(measurement_names[8]), today));
} while (true);
delete[] literal;
EXPECT_EQ(cur_record_num, row_num);
reader.destroy_query_data_set(qds);
ASSERT_EQ(reader.close(), E_OK);
}
TEST_F(TsFileWriterTest, RegisterTimeSeries) {
std::string device_path = "device1";
std::string measurement_name = "temperature";
common::TSDataType data_type = common::TSDataType::INT32;
common::TSEncoding encoding = common::TSEncoding::PLAIN;
common::CompressionType compression_type =
common::CompressionType::UNCOMPRESSED;
ASSERT_EQ(tsfile_writer_->register_timeseries(
device_path,
storage::MeasurementSchema(measurement_name, data_type,
encoding, compression_type)),
E_OK);
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
ASSERT_EQ(tsfile_writer_->close(), E_OK);
}
TEST_F(TsFileWriterTest, MultiFlushWriteAndRead) {
std::string device_path = "device1";
std::string measurement_name = "temperature";
tsfile_writer_->register_timeseries(
device_path,
storage::MeasurementSchema(measurement_name, common::TSDataType::INT64,
common::TSEncoding::PLAIN,
common::CompressionType::UNCOMPRESSED));
const int64_t start_time = 1622505600000LL;
const int row_count = 200000;
for (int i = 0; i < row_count; ++i) {
TsRecord record(start_time + i * 1000, device_path);
record.add_point(measurement_name, static_cast<int64_t>(i));
ASSERT_EQ(tsfile_writer_->write_record(record), E_OK);
if ((i + 1) % 10000 == 0) {
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
}
}
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
ASSERT_EQ(tsfile_writer_->close(), E_OK);
storage::TsFileReader reader;
ASSERT_EQ(reader.open(file_name_), E_OK);
std::vector<std::string> select_list = {"device1.temperature"};
storage::ResultSet* tmp_qds = nullptr;
ASSERT_EQ(reader.query(select_list, start_time,
start_time + (row_count - 1) * 1000 + 1, tmp_qds),
E_OK);
auto* qds = static_cast<QDSWithoutTimeGenerator*>(tmp_qds);
int64_t read_rows = 0;
bool has_next = false;
while (true) {
ASSERT_EQ(qds->next(has_next), E_OK);
if (!has_next) {
break;
}
ASSERT_EQ(qds->get_value<int64_t>(1), start_time + read_rows * 1000);
ASSERT_EQ(qds->get_value<int64_t>(2), read_rows);
++read_rows;
}
ASSERT_EQ(read_rows, row_count);
reader.destroy_query_data_set(qds);
ASSERT_EQ(reader.close(), E_OK);
}
#if defined(ENABLE_ZLIB) && defined(ENABLE_SNAPPY) && defined(ENABLE_LZ4) && \
defined(ENABLE_LZOKAY)
TEST_F(TsFileWriterTest, WriteDiffrentTypeCombination) {
std::string device_path = "device1";
std::string measurement_name = "temperature";
std::vector<TSDataType> data_types = {TSDataType::INT32, TSDataType::INT64,
TSDataType::FLOAT,
TSDataType::DOUBLE};
std::vector<TSEncoding> encodings = {TSEncoding::PLAIN,
TSEncoding::TS_2DIFF};
std::vector<CompressionType> compression_types = {
CompressionType::UNCOMPRESSED, CompressionType::SNAPPY,
CompressionType::GZIP, CompressionType::LZ4};
std::vector<MeasurementSchema> schema_vecs;
schema_vecs.reserve(data_types.size() * encodings.size() *
compression_types.size());
int idx = 0;
for (auto data_type : data_types) {
for (auto encoding_type : encodings) {
for (auto compression_type : compression_types) {
schema_vecs.emplace_back(MeasurementSchema(
measurement_name + std::to_string(idx), data_type,
encoding_type, compression_type));
tsfile_writer_->register_timeseries(device_path,
schema_vecs[idx++]);
}
}
}
char* literal = new char[std::strlen("literal") + 1];
std::strcpy(literal, "literal");
String literal_str(literal, std::strlen("literal"));
for (size_t l = 0; l < 100; ++l) {
TsRecord record(1622505600000 + 1 * 1000, device_path);
for (size_t i = 0; i < schema_vecs.size(); ++i) {
if (schema_vecs[i].data_type_ == TSDataType::INT32) {
record.add_point(schema_vecs[i].measurement_name_, (int32_t)i);
} else if (schema_vecs[i].data_type_ == TSDataType::FLOAT) {
record.add_point(schema_vecs[i].measurement_name_, 3.14);
} else if (schema_vecs[i].data_type_ == TSDataType::DOUBLE) {
record.add_point(schema_vecs[i].measurement_name_, 3.1415926);
} else if (schema_vecs[i].data_type_ == TSDataType::BOOLEAN) {
record.add_point(schema_vecs[i].measurement_name_, true);
} else if (schema_vecs[i].data_type_ == TSDataType::STRING) {
record.add_point(schema_vecs[i].measurement_name_, literal_str);
}
}
ASSERT_EQ(tsfile_writer_->write_record(record), E_OK);
}
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
ASSERT_EQ(tsfile_writer_->close(), E_OK);
delete[] literal;
}
#endif
TEST_F(TsFileWriterTest, WriteMultipleTabletsMultiFlush) {
common::config_set_max_degree_of_index_node(3);
const int device_num = 20;
const int measurement_num = 20;
int max_tablet_num = 100;
std::vector<std::vector<MeasurementSchema>> schema_vecs(
device_num, std::vector<MeasurementSchema>(measurement_num));
for (int i = 0; i < device_num; i++) {
std::string device_name = "test_device" + std::to_string(i);
for (int j = 0; j < measurement_num; j++) {
std::string measure_name = "measurement" + std::to_string(j);
schema_vecs[i][j] =
MeasurementSchema(measure_name, common::TSDataType::INT32,
common::TSEncoding::PLAIN,
common::CompressionType::UNCOMPRESSED);
tsfile_writer_->register_timeseries(
device_name, storage::MeasurementSchema(
measure_name, common::TSDataType::INT32,
common::TSEncoding::PLAIN,
common::CompressionType::UNCOMPRESSED));
}
}
for (int tablet_num = 0; tablet_num < max_tablet_num; tablet_num++) {
for (int i = 0; i < device_num; i++) {
std::string device_name = "test_device" + std::to_string(i);
storage::Tablet tablet(
device_name,
std::make_shared<std::vector<MeasurementSchema>>(
schema_vecs[i]),
1);
for (int j = 0; j < measurement_num; j++) {
tablet.add_timestamp(0, 16225600000 + tablet_num * 100);
tablet.add_value(0, j, static_cast<int32_t>(tablet_num));
}
ASSERT_EQ(tsfile_writer_->write_tablet(tablet), E_OK);
}
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
}
ASSERT_EQ(tsfile_writer_->close(), E_OK);
std::vector<storage::Path> select_list;
for (int i = 0; i < device_num; i++) {
for (int j = 0; j < measurement_num; ++j) {
std::string device_name = "test_device" + std::to_string(i);
std::string measure_name = "measurement" + std::to_string(j);
storage::Path path(device_name, measure_name);
select_list.push_back(path);
}
}
storage::QueryExpression* query_expr =
storage::QueryExpression::create(select_list, nullptr);
storage::TsFileReader reader;
int ret = reader.open(file_name_);
ASSERT_EQ(ret, common::E_OK);
storage::ResultSet* tmp_qds = nullptr;
ret = reader.query(query_expr, tmp_qds);
auto* qds = (QDSWithoutTimeGenerator*)tmp_qds;
storage::RowRecord* record;
int max_rows = max_tablet_num * 1;
bool has_next = false;
for (int cur_row = 0; cur_row < max_rows; cur_row++) {
if (IS_FAIL(qds->next(has_next)) || !has_next) {
break;
}
record = qds->get_row_record();
int size = record->get_fields()->size();
for (int i = 0; i < size; ++i) {
if (i == 0) {
EXPECT_EQ(std::to_string(record->get_timestamp()),
field_to_string(record->get_field(i)));
continue;
}
EXPECT_EQ(std::to_string(cur_row),
field_to_string(record->get_field(i)));
}
}
reader.destroy_query_data_set(qds);
}
TEST_F(TsFileWriterTest, WriteMultipleTabletsAlignedMultiFlush) {
common::config_set_max_degree_of_index_node(3);
const int device_num = 20;
const int measurement_num = 20;
int max_tablet_num = 100;
std::vector<std::vector<MeasurementSchema>> schema_vecs(
device_num, std::vector<MeasurementSchema>(measurement_num));
for (int i = 0; i < device_num; i++) {
std::string device_name = "test_device" + std::to_string(i);
for (int j = 0; j < measurement_num; j++) {
std::string measure_name = "measurement" + std::to_string(j);
schema_vecs[i][j] =
MeasurementSchema(measure_name, common::TSDataType::INT32,
common::TSEncoding::PLAIN,
common::CompressionType::UNCOMPRESSED);
tsfile_writer_->register_aligned_timeseries(
device_name, storage::MeasurementSchema(
measure_name, common::TSDataType::INT32,
common::TSEncoding::PLAIN,
common::CompressionType::UNCOMPRESSED));
}
}
for (int tablet_num = 0; tablet_num < max_tablet_num; tablet_num++) {
for (int i = 0; i < device_num; i++) {
std::string device_name = "test_device" + std::to_string(i);
storage::Tablet tablet(
device_name,
std::make_shared<std::vector<MeasurementSchema>>(
schema_vecs[i]),
1);
for (int j = 0; j < measurement_num; j++) {
tablet.add_timestamp(0, 16225600000 + tablet_num * 100);
tablet.add_value(0, j, static_cast<int32_t>(tablet_num));
}
ASSERT_EQ(tsfile_writer_->write_tablet_aligned(tablet), E_OK);
}
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
}
ASSERT_EQ(tsfile_writer_->close(), E_OK);
std::vector<storage::Path> select_list;
for (int i = 0; i < device_num; i++) {
for (int j = 0; j < measurement_num; ++j) {
std::string device_name = "test_device" + std::to_string(i);
std::string measure_name = "measurement" + std::to_string(j);
storage::Path path(device_name, measure_name);
select_list.push_back(path);
}
}
storage::QueryExpression* query_expr =
storage::QueryExpression::create(select_list, nullptr);
storage::TsFileReader reader;
int ret = reader.open(file_name_);
ASSERT_EQ(ret, common::E_OK);
storage::ResultSet* tmp_qds = nullptr;
ret = reader.query(query_expr, tmp_qds);
ASSERT_EQ(ret, common::E_OK);
auto* qds = (QDSWithoutTimeGenerator*)tmp_qds;
storage::RowRecord* record;
int max_rows = max_tablet_num * 1;
bool has_next = false;
for (int cur_row = 0; cur_row < max_rows; cur_row++) {
if (IS_FAIL(qds->next(has_next)) || !has_next) {
break;
}
record = qds->get_row_record();
int size = record->get_fields()->size();
for (int i = 0; i < size; ++i) {
if (i == 0) {
ASSERT_EQ(field_to_string(record->get_field(0)),
std::to_string(record->get_timestamp()));
continue;
}
EXPECT_EQ(std::to_string(cur_row),
field_to_string(record->get_field(i)));
}
}
reader.destroy_query_data_set(qds);
}
TEST_F(TsFileWriterTest, WriteMultipleTabletsInt64) {
const int device_num = 50;
const int measurement_num = 50;
std::vector<MeasurementSchema> schema_vec[50];
for (int i = 0; i < device_num; i++) {
std::string device_name = "test_device" + std::to_string(i);
for (int j = 0; j < measurement_num; j++) {
std::string measure_name = "measurement" + std::to_string(j);
schema_vec[i].push_back(
MeasurementSchema(measure_name, common::TSDataType::INT64,
common::TSEncoding::PLAIN,
common::CompressionType::UNCOMPRESSED));
tsfile_writer_->register_timeseries(
device_name, storage::MeasurementSchema(
measure_name, common::TSDataType::INT64,
common::TSEncoding::PLAIN,
common::CompressionType::UNCOMPRESSED));
}
}
for (int i = 0; i < device_num; i++) {
std::string device_name = "test_device" + std::to_string(i);
int max_rows = 100;
storage::Tablet tablet(
device_name,
std::make_shared<std::vector<MeasurementSchema>>(schema_vec[i]),
max_rows);
for (int j = 0; j < measurement_num; j++) {
for (int row = 0; row < max_rows; row++) {
tablet.add_timestamp(row, 16225600 + row);
}
for (int row = 0; row < max_rows; row++) {
tablet.add_value(row, j, static_cast<int64_t>(row));
}
}
ASSERT_EQ(tsfile_writer_->write_tablet(tablet), E_OK);
}
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
ASSERT_EQ(tsfile_writer_->close(), E_OK);
}
TEST_F(TsFileWriterTest, WriteMultipleTabletsDouble) {
const int device_num = 50;
const int measurement_num = 50;
std::vector<MeasurementSchema> schema_vec[50];
for (int i = 0; i < device_num; i++) {
std::string device_name = "test_device" + std::to_string(i);
for (int j = 0; j < measurement_num; j++) {
std::string measure_name = "measurement" + std::to_string(j);
schema_vec[i].push_back(
MeasurementSchema(measure_name, common::TSDataType::DOUBLE,
common::TSEncoding::PLAIN,
common::CompressionType::UNCOMPRESSED));
tsfile_writer_->register_timeseries(
device_name, storage::MeasurementSchema(
measure_name, common::TSDataType::DOUBLE,
common::TSEncoding::PLAIN,
common::CompressionType::UNCOMPRESSED));
}
}
for (int i = 0; i < device_num; i++) {
std::string device_name = "test_device" + std::to_string(i);
int max_rows = 200;
storage::Tablet tablet(
device_name,
std::make_shared<std::vector<MeasurementSchema>>(schema_vec[i]),
max_rows);
for (int j = 0; j < measurement_num; j++) {
for (int row = 0; row < max_rows; row++) {
tablet.add_timestamp(row, 16225600 + row);
}
for (int row = 0; row < max_rows; row++) {
tablet.add_value(row, j, static_cast<double>(row) + 1.0);
}
}
ASSERT_EQ(tsfile_writer_->write_tablet(tablet), E_OK);
}
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
ASSERT_EQ(tsfile_writer_->close(), E_OK);
}
// Regression: write_column() is the null fallback of the non-aligned batch
// path (write_column_batch -> has_null -> write_column). It used to handle
// only BOOLEAN/INT32/INT64/FLOAT/DOUBLE/STRING and ASSERT(false) otherwise;
// in NDEBUG that assert is a no-op, so a non-aligned TEXT/BLOB/DATE/TIMESTAMP
// column that contained a null silently dropped every row of that column.
// This writes a TEXT column with a null in the middle and verifies the two
// non-null rows survive the round trip.
TEST_F(TsFileWriterTest, NonAlignedTextColumnWithNullIsNotDropped) {
// Non-const: storage::Path's ctor takes non-const std::string&.
std::string device = "root.dev_text_null";
std::string measure = "s_text";
tsfile_writer_->register_timeseries(
device, MeasurementSchema(measure, common::TSDataType::TEXT,
common::TSEncoding::PLAIN,
common::CompressionType::UNCOMPRESSED));
std::vector<MeasurementSchema> schema_vec;
schema_vec.emplace_back(measure, common::TSDataType::TEXT,
common::TSEncoding::PLAIN,
common::CompressionType::UNCOMPRESSED);
const int max_rows = 3;
storage::Tablet tablet(
device, std::make_shared<std::vector<MeasurementSchema>>(schema_vec),
max_rows);
for (int row = 0; row < max_rows; row++) {
ASSERT_EQ(tablet.add_timestamp(row, 1000 + row), E_OK);
}
// Rows 0 and 2 get values; row 1 is left untouched, so its not-null bit
// stays set (default) — that is the null that forces the write_column
// fallback.
char buf0[] = "v0";
char buf2[] = "v2";
String s0(buf0, 2), s2(buf2, 2);
ASSERT_EQ(tablet.add_value(0, 0u, s0), E_OK);
ASSERT_EQ(tablet.add_value(2, 0u, s2), E_OK);
ASSERT_EQ(tsfile_writer_->write_tablet(tablet), E_OK);
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
ASSERT_EQ(tsfile_writer_->close(), E_OK);
storage::TsFileReader reader;
ASSERT_EQ(reader.open(file_name_), E_OK);
std::vector<storage::Path> select_list{storage::Path(device, measure)};
storage::QueryExpression* query_expr =
storage::QueryExpression::create(select_list, nullptr);
storage::ResultSet* tmp_qds = nullptr;
ASSERT_EQ(reader.query(query_expr, tmp_qds), E_OK);
auto* qds = (QDSWithoutTimeGenerator*)tmp_qds;
// The regression signal is row survival: before the fix write_column hit
// ASSERT(false) on TEXT (a no-op in NDEBUG), so the column was dropped and
// this query returned 0 rows. TEXT shares the identical (proven) string
// write path as STRING, so the two surviving rows at the right timestamps
// confirm the fix. field(1) is the value column, but field(0) is non-null
// here too — the result row carries the timestamp as field(0).
std::vector<int64_t> times;
bool has_next = false;
while (IS_SUCC(qds->next(has_next)) && has_next) {
storage::RowRecord* rec = qds->get_row_record();
times.push_back(rec->get_timestamp());
}
reader.destroy_query_data_set(qds);
reader.close();
ASSERT_EQ(times.size(), 2u);
EXPECT_EQ(times[0], 1000);
EXPECT_EQ(times[1], 1002);
}
TEST_F(TsFileWriterTest, FlushMultipleDevice) {
const int device_num = 50;
const int measurement_num = 50;
const int max_rows = 100;
std::vector<MeasurementSchema> schema_vec[50];
for (int i = 0; i < device_num; i++) {
std::string device_name = "test_device" + std::to_string(i);
for (int j = 0; j < measurement_num; j++) {
std::string measure_name = "measurement" + std::to_string(j);
schema_vec[i].emplace_back(measure_name, common::TSDataType::INT64,
common::TSEncoding::PLAIN,
common::CompressionType::UNCOMPRESSED);
tsfile_writer_->register_timeseries(
device_name,
MeasurementSchema(measure_name, common::TSDataType::INT64,
common::TSEncoding::PLAIN,
common::CompressionType::UNCOMPRESSED));
}
}
for (int i = 0; i < device_num; i++) {
std::string device_name = "test_device" + std::to_string(i);
storage::Tablet tablet(
device_name,
std::make_shared<std::vector<MeasurementSchema>>(schema_vec[i]),
max_rows);
for (int j = 0; j < measurement_num; j++) {
for (int row = 0; row < max_rows; row++) {
tablet.add_timestamp(row, 16225600 + row);
}
for (int row = 0; row < max_rows; row++) {
tablet.add_value(row, j, static_cast<int64_t>(row));
}
}
ASSERT_EQ(tsfile_writer_->write_tablet(tablet), E_OK);
// flush after write tablet to check whether write empty chunk
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
}
ASSERT_EQ(tsfile_writer_->close(), E_OK);
std::vector<storage::Path> select_list;
for (int i = 0; i < device_num; i++) {
std::string device_name = "test_device" + std::to_string(i);
for (int j = 0; j < measurement_num; j++) {
std::string measurement_name = "measurement" + std::to_string(j);
storage::Path path(device_name, measurement_name);
select_list.push_back(path);
}
}
storage::QueryExpression* query_expr =
storage::QueryExpression::create(select_list, nullptr);
storage::TsFileReader reader;
int ret = reader.open(file_name_);
ASSERT_EQ(ret, common::E_OK);
storage::ResultSet* tmp_qds = nullptr;
ret = reader.query(query_expr, tmp_qds);
auto* qds = (QDSWithoutTimeGenerator*)tmp_qds;
storage::RowRecord* record;
int64_t cur_record_num = 0;
bool has_next = false;
do {
if (IS_FAIL(qds->next(has_next)) || !has_next) {
break;
}
record = qds->get_row_record();
// if empty chunk is written, the timestamp should be NULL
if (!record) {
break;
}
EXPECT_EQ(record->get_timestamp(), 16225600 + cur_record_num);
cur_record_num++;
} while (true);
EXPECT_EQ(cur_record_num, max_rows);
reader.destroy_query_data_set(qds);
}
TEST_F(TsFileWriterTest, AnalyzeTsfileForload) {
// estimate_max_mem_size() now reflects the real 64 KiB-page footprint of
// each per-measurement output stream. 50 devices × 50 measurements ×
// 2 streams × 64 KiB = ~320 MiB, well past the 128 MiB default
// chunk_group_size_threshold_ — without raising the cap the auto-flush
// would fire mid-write and the post-write hasData() check below would
// observe a freshly drained chunk writer. Lift the cap for the
// duration of this smoke test so the original semantics still apply.
uint32_t prev_threshold =
common::g_config_value_.chunk_group_size_threshold_;
struct Guard {
uint32_t prev;
~Guard() { common::g_config_value_.chunk_group_size_threshold_ = prev; }
} guard{prev_threshold};
common::g_config_value_.chunk_group_size_threshold_ =
2ULL * 1024 * 1024 * 1024;
const int device_num = 50;
const int measurement_num = 50;
const int max_rows = 100;
std::vector<MeasurementSchema> schema_vec[50];
for (int i = 0; i < device_num; i++) {
std::string device_name = "test_device" + std::to_string(i);
for (int j = 0; j < measurement_num; j++) {
std::string measure_name = "measurement" + std::to_string(j);
schema_vec[i].push_back(
MeasurementSchema(measure_name, common::TSDataType::INT64,
common::TSEncoding::PLAIN,
common::CompressionType::UNCOMPRESSED));
tsfile_writer_->register_timeseries(
device_name,
MeasurementSchema(measure_name, common::TSDataType::INT64,
common::TSEncoding::PLAIN,
common::CompressionType::UNCOMPRESSED));
}
}
for (int i = 0; i < device_num; i++) {
std::string device_name = "test_device" + std::to_string(i);
storage::Tablet tablet(
device_name,
std::make_shared<std::vector<MeasurementSchema>>(schema_vec[i]),
max_rows);
for (int j = 0; j < measurement_num; j++) {
for (int row = 0; row < max_rows; row++) {
tablet.add_timestamp(row, 16225600 + row);
}
for (int row = 0; row < max_rows; row++) {
tablet.add_value(row, j, static_cast<int64_t>(row));
}
}
ASSERT_EQ(tsfile_writer_->write_tablet(tablet), E_OK);
}
auto schemas = tsfile_writer_->get_schema_group_map();
ASSERT_EQ(schemas->size(), 50);
for (const auto& device_iter : *schemas) {
for (const auto& chunk_iter :
device_iter.second->measurement_schema_map_) {
ASSERT_NE(chunk_iter.second->chunk_writer_, nullptr);
ASSERT_TRUE(chunk_iter.second->chunk_writer_->hasData());
}
}
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
ASSERT_EQ(tsfile_writer_->close(), E_OK);
}
TEST_F(TsFileWriterTest, FlushWithoutWriteAfterRegisterTS) {
std::string device_path = "device1";
std::string measurement_name = "temperature";
common::TSDataType data_type = common::TSDataType::INT32;
common::TSEncoding encoding = common::TSEncoding::PLAIN;
common::CompressionType compression_type =
common::CompressionType::UNCOMPRESSED;
ASSERT_EQ(tsfile_writer_->register_timeseries(
device_path,
storage::MeasurementSchema(measurement_name, data_type,
encoding, compression_type)),
E_OK);
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
ASSERT_EQ(tsfile_writer_->close(), E_OK);
}
TEST_F(TsFileWriterTest, WriteAlignedTimeseries) {
int measurement_num = 100, row_num = 150;
std::string device_name = "device";
std::vector<std::string> measurement_names;
for (int i = 0; i < measurement_num; i++) {
measurement_names.emplace_back("temperature" + std::to_string(i));
}
common::TSDataType data_type = common::TSDataType::INT32;
common::TSEncoding encoding = common::TSEncoding::PLAIN;
common::CompressionType compression_type =
common::CompressionType::UNCOMPRESSED;
std::vector<MeasurementSchema*> measurement_schema_vec;
for (const auto& measurement_name : measurement_names) {
auto* ms = new MeasurementSchema(measurement_name, data_type, encoding,
compression_type);
measurement_schema_vec.push_back(ms);
}
tsfile_writer_->register_aligned_timeseries(device_name,
measurement_schema_vec);
for (int i = 0; i < row_num; ++i) {
TsRecord record(1622505600000 + i * 1000, device_name);
for (const auto& measurement_name : measurement_names) {
record.add_point(measurement_name, (int32_t)i);
}
ASSERT_EQ(tsfile_writer_->write_record_aligned(record), E_OK);
}
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
ASSERT_EQ(tsfile_writer_->close(), E_OK);
std::vector<storage::Path> select_list;
for (int i = 0; i < measurement_num; ++i) {
std::string measurement_name = "temperature" + std::to_string(i);
storage::Path path(device_name, measurement_name);
select_list.push_back(path);
}
storage::QueryExpression* query_expr =
storage::QueryExpression::create(select_list, nullptr);
storage::TsFileReader reader;
int ret = reader.open(file_name_);
ASSERT_EQ(ret, common::E_OK);
storage::ResultSet* tmp_qds = nullptr;
ret = reader.query(query_expr, tmp_qds);
auto* qds = (QDSWithoutTimeGenerator*)tmp_qds;
storage::RowRecord* record;
bool has_next = false;
for (int cur_row = 0; cur_row < row_num; cur_row++) {
if (IS_FAIL(qds->next(has_next)) || !has_next) {
break;
}
record = qds->get_row_record();
int size = record->get_fields()->size();
for (int i = 0; i < size; ++i) {
if (i == 0) {
EXPECT_EQ(std::to_string(record->get_timestamp()),
field_to_string(record->get_field(i)));
continue;
}
EXPECT_EQ(std::to_string(cur_row),
field_to_string(record->get_field(i)));
}
}
reader.destroy_query_data_set(qds);
}
/*
* Aligned page seal synchronization tests.
*
* In the aligned model, time page and every value page must seal together
* so that each chunk has the same number of pages. Without synchronization,
* a threshold hit on one page (point-count or memory) would seal only that
* page, producing misaligned page counts and corrupt reads.
*
* Three sub-cases:
* 1. Time page reaches point-count threshold first; value pages have
* partial nulls so their non-null statistic count is lower and they
* would NOT seal on their own.
* 2. Time page reaches memory threshold first; value pages are mostly
* null so their encoded-data memory is much smaller.
* 3. A value page (STRING, large per-row memory) reaches memory
* threshold first; time page and other value pages have not.
*/
// Case 1: time page seals by point-count; value pages with partial nulls
// have fewer non-null points (statistic count) and would not self-seal.
// Sync mechanism must force all value pages to seal together.
TEST_F(TsFileWriterTest, AlignedSealSync_PointCountWithNulls) {
uint32_t prev_pt = g_config_value_.page_writer_max_point_num_;
uint32_t prev_mem = g_config_value_.page_writer_max_memory_bytes_;
struct Guard {
uint32_t pt, mem;
~Guard() {
g_config_value_.page_writer_max_point_num_ = pt;
g_config_value_.page_writer_max_memory_bytes_ = mem;
}
} guard{prev_pt, prev_mem};
g_config_value_.page_writer_max_point_num_ = 10;
g_config_value_.page_writer_max_memory_bytes_ = 1024 * 1024;
std::string device_name = "device_pt_null";
std::vector<std::string> mnames = {"s0", "s1", "s2"};
std::vector<MeasurementSchema*> schemas;
for (auto& n : mnames) {
schemas.push_back(new MeasurementSchema(n, INT64, PLAIN, UNCOMPRESSED));
}
tsfile_writer_->register_aligned_timeseries(device_name, schemas);
// s0: always non-null -> 10 non-null per 10-row page, self-seals
// s1: null on even rows -> 5 non-null per page, won't self-seal
// s2: null except every 5th row -> 2 non-null per page, won't self-seal
int row_num = 30;
for (int i = 0; i < row_num; ++i) {
TsRecord record(1622505600000 + i, device_name);
record.add_point(mnames[0], static_cast<int64_t>(i));
if (i % 2 != 0) {
record.add_point(mnames[1], static_cast<int64_t>(i * 10));
} else {
record.points_.emplace_back(DataPoint(mnames[1]));
}
if (i % 5 == 0) {
record.add_point(mnames[2], static_cast<int64_t>(i * 100));
} else {
record.points_.emplace_back(DataPoint(mnames[2]));
}
ASSERT_EQ(tsfile_writer_->write_record_aligned(record), E_OK);
}
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
ASSERT_EQ(tsfile_writer_->close(), E_OK);
std::vector<storage::Path> select_list;
for (auto& n : mnames) {
select_list.emplace_back(device_name, n);
}
storage::QueryExpression* qe =
storage::QueryExpression::create(select_list, nullptr);
storage::TsFileReader reader;
ASSERT_EQ(reader.open(file_name_), E_OK);
storage::ResultSet* tmp_qds = nullptr;
ASSERT_EQ(reader.query(qe, tmp_qds), E_OK);
auto* qds = (QDSWithoutTimeGenerator*)tmp_qds;
bool has_next = false;
int64_t cur_row = 0;
while (IS_SUCC(qds->next(has_next)) && has_next) {
auto* rec = qds->get_row_record();
ASSERT_NE(rec, nullptr);
EXPECT_EQ(rec->get_timestamp(), 1622505600000 + cur_row);
EXPECT_EQ(field_to_string(rec->get_field(1)), std::to_string(cur_row));
if (cur_row % 2 != 0) {
EXPECT_EQ(field_to_string(rec->get_field(2)),
std::to_string(cur_row * 10));
}
if (cur_row % 5 == 0) {
EXPECT_EQ(field_to_string(rec->get_field(3)),
std::to_string(cur_row * 100));
}
cur_row++;
}
EXPECT_EQ(cur_row, row_num);
reader.destroy_query_data_set(qds);
ASSERT_EQ(reader.close(), E_OK);
}
// Case 2: time page seals by memory threshold first. Value pages are mostly
// null so their encoded-value memory grows much slower than the time page
// (INT64 PLAIN = 8 bytes/point). Time page hits 512 bytes at ~64 points;
// value pages with 1 non-null every 20 rows only have ~24 bytes of value
// data at that point. Sync must force all value pages to seal.
TEST_F(TsFileWriterTest, AlignedSealSync_TimeMemoryFirst) {
uint32_t prev_pt = g_config_value_.page_writer_max_point_num_;
uint32_t prev_mem = g_config_value_.page_writer_max_memory_bytes_;
struct Guard {
uint32_t pt, mem;
~Guard() {
g_config_value_.page_writer_max_point_num_ = pt;
g_config_value_.page_writer_max_memory_bytes_ = mem;
}
} guard{prev_pt, prev_mem};
g_config_value_.page_writer_max_point_num_ = 10000;
g_config_value_.page_writer_max_memory_bytes_ = 512;
std::string device_name = "device_time_mem";
std::vector<std::string> mnames = {"s0", "s1"};
std::vector<MeasurementSchema*> schemas;
for (auto& n : mnames) {
schemas.push_back(new MeasurementSchema(n, INT64, PLAIN, UNCOMPRESSED));
}
tsfile_writer_->register_aligned_timeseries(device_name, schemas);
int row_num = 200;
for (int i = 0; i < row_num; ++i) {
TsRecord record(1622505600000 + i, device_name);
if (i % 20 == 0) {
record.add_point(mnames[0], static_cast<int64_t>(i));
record.add_point(mnames[1], static_cast<int64_t>(i * 10));
} else {
record.points_.emplace_back(DataPoint(mnames[0]));
record.points_.emplace_back(DataPoint(mnames[1]));
}
ASSERT_EQ(tsfile_writer_->write_record_aligned(record), E_OK);
}
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
ASSERT_EQ(tsfile_writer_->close(), E_OK);
std::vector<storage::Path> select_list;
for (auto& n : mnames) {
select_list.emplace_back(device_name, n);
}
storage::QueryExpression* qe =
storage::QueryExpression::create(select_list, nullptr);
storage::TsFileReader reader;
ASSERT_EQ(reader.open(file_name_), E_OK);
storage::ResultSet* tmp_qds = nullptr;
ASSERT_EQ(reader.query(qe, tmp_qds), E_OK);
auto* qds = (QDSWithoutTimeGenerator*)tmp_qds;
bool has_next = false;
int64_t cur_row = 0;
while (IS_SUCC(qds->next(has_next)) && has_next) {
auto* rec = qds->get_row_record();
ASSERT_NE(rec, nullptr);
EXPECT_EQ(rec->get_timestamp(), 1622505600000 + cur_row);
if (cur_row % 20 == 0) {
EXPECT_EQ(field_to_string(rec->get_field(1)),
std::to_string(cur_row));
EXPECT_EQ(field_to_string(rec->get_field(2)),
std::to_string(cur_row * 10));
}
cur_row++;
}
EXPECT_EQ(cur_row, row_num);
reader.destroy_query_data_set(qds);
ASSERT_EQ(reader.close(), E_OK);
}
// Case 3: a value page (STRING type, ~104 bytes/point with PLAIN encoding)
// seals by memory threshold before the time page (INT64, 8 bytes/point).
// With threshold=512, STRING value page seals at ~5 points while time page
// only has ~40 bytes. Sync must force time page and other value pages to seal.
TEST_F(TsFileWriterTest, AlignedSealSync_ValueMemoryFirst) {
uint32_t prev_pt = g_config_value_.page_writer_max_point_num_;
uint32_t prev_mem = g_config_value_.page_writer_max_memory_bytes_;
struct Guard {
uint32_t pt, mem;
~Guard() {
g_config_value_.page_writer_max_point_num_ = pt;
g_config_value_.page_writer_max_memory_bytes_ = mem;
}
} guard{prev_pt, prev_mem};
g_config_value_.page_writer_max_point_num_ = 10000;
g_config_value_.page_writer_max_memory_bytes_ = 512;
std::string device_name = "device_val_mem";
std::vector<MeasurementSchema*> schemas;
schemas.push_back(new MeasurementSchema("s0", INT64, PLAIN, UNCOMPRESSED));
schemas.push_back(new MeasurementSchema("s1", STRING, PLAIN, UNCOMPRESSED));
tsfile_writer_->register_aligned_timeseries(device_name, schemas);
char* long_buf = new char[101];
memset(long_buf, 'A', 100);
long_buf[100] = '\0';
common::String str_val(long_buf, 100);
int row_num = 100;
for (int i = 0; i < row_num; ++i) {
TsRecord record(1622505600000 + i, device_name);
record.add_point(std::string("s0"), static_cast<int64_t>(i));
record.add_point(std::string("s1"), str_val);
ASSERT_EQ(tsfile_writer_->write_record_aligned(record), E_OK);
}
delete[] long_buf;
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
ASSERT_EQ(tsfile_writer_->close(), E_OK);
std::string s0("s0"), s1("s1");
std::vector<storage::Path> select_list;
select_list.emplace_back(device_name, s0);
select_list.emplace_back(device_name, s1);
storage::QueryExpression* qe =
storage::QueryExpression::create(select_list, nullptr);
storage::TsFileReader reader;
ASSERT_EQ(reader.open(file_name_), E_OK);
storage::ResultSet* tmp_qds = nullptr;
ASSERT_EQ(reader.query(qe, tmp_qds), E_OK);
auto* qds = (QDSWithoutTimeGenerator*)tmp_qds;
bool has_next = false;
int64_t cur_row = 0;
while (IS_SUCC(qds->next(has_next)) && has_next) {
auto* rec = qds->get_row_record();
ASSERT_NE(rec, nullptr);
EXPECT_EQ(rec->get_timestamp(), 1622505600000 + cur_row);
EXPECT_EQ(field_to_string(rec->get_field(1)), std::to_string(cur_row));
cur_row++;
}
EXPECT_EQ(cur_row, row_num);
reader.destroy_query_data_set(qds);
ASSERT_EQ(reader.close(), E_OK);
}
// Regression: write_tablet_aligned() writes the entire time column first and
// then each value column. With memory-based auto-seal still active, a large
// STRING value column hits the memory threshold mid-batch (say at row 5),
// while the INT64 time column does not seal until row page_writer_max_point
// is reached. Those divergent seals stamp misaligned page boundaries onto
// the file and read-back returns wrong values per row. Suppressing
// memory-driven seals during the batch should keep all pages count-aligned.
TEST_F(TsFileWriterTest, AlignedSealSync_TabletLargeStringValueMemoryFirst) {
uint32_t prev_pt = g_config_value_.page_writer_max_point_num_;
uint32_t prev_mem = g_config_value_.page_writer_max_memory_bytes_;
struct Guard {
uint32_t pt, mem;
~Guard() {
g_config_value_.page_writer_max_point_num_ = pt;
g_config_value_.page_writer_max_memory_bytes_ = mem;
}
} guard{prev_pt, prev_mem};
// Big point cap, tiny memory cap: time chunk (INT64 PLAIN, 8B/point) never
// hits memory before it reaches the point cap, while the STRING value
// chunk crosses the memory threshold within a handful of rows.
g_config_value_.page_writer_max_point_num_ = 10000;
g_config_value_.page_writer_max_memory_bytes_ = 512;
std::string device_name = "device_tablet_str";
std::vector<MeasurementSchema> schema_vec;
schema_vec.emplace_back("s0", INT64, PLAIN, UNCOMPRESSED);
schema_vec.emplace_back("s1", STRING, PLAIN, UNCOMPRESSED);
schema_vec.emplace_back("s2", INT64, PLAIN, UNCOMPRESSED);
{
std::vector<MeasurementSchema*> reg;
for (auto& s : schema_vec) reg.push_back(new MeasurementSchema(s));
tsfile_writer_->register_aligned_timeseries(device_name, reg);
}
const int row_num = 200;
Tablet tablet(device_name,
std::make_shared<std::vector<MeasurementSchema>>(schema_vec),
row_num);
char* long_buf = new char[101];
memset(long_buf, 'A', 100);
long_buf[100] = '\0';
common::String str_val(long_buf, 100);
for (int i = 0; i < row_num; ++i) {
ASSERT_EQ(tablet.add_timestamp(i, 1622505600000 + i), E_OK);
ASSERT_EQ(tablet.add_value(i, 0u, static_cast<int64_t>(i)), E_OK);
// Sparse string column: every third row is null so we also exercise
// the bitmap path through the memory-pressured value page.
if (i % 3 != 0) {
ASSERT_EQ(tablet.add_value(i, 1u, str_val), E_OK);
}
ASSERT_EQ(tablet.add_value(i, 2u, static_cast<int64_t>(i * 10)), E_OK);
}
delete[] long_buf;
ASSERT_EQ(tsfile_writer_->write_tablet_aligned(tablet), E_OK);
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
ASSERT_EQ(tsfile_writer_->close(), E_OK);
std::string s0("s0"), s1("s1"), s2("s2");
std::vector<storage::Path> select_list;
select_list.emplace_back(device_name, s0);
select_list.emplace_back(device_name, s1);
select_list.emplace_back(device_name, s2);
storage::QueryExpression* qe =
storage::QueryExpression::create(select_list, nullptr);
storage::TsFileReader reader;
ASSERT_EQ(reader.open(file_name_), E_OK);
storage::ResultSet* tmp_qds = nullptr;
ASSERT_EQ(reader.query(qe, tmp_qds), E_OK);
auto* qds = (QDSWithoutTimeGenerator*)tmp_qds;
bool has_next = false;
int64_t cur_row = 0;
while (IS_SUCC(qds->next(has_next)) && has_next) {
auto* rec = qds->get_row_record();
ASSERT_NE(rec, nullptr);
EXPECT_EQ(rec->get_timestamp(), 1622505600000 + cur_row);
EXPECT_EQ(field_to_string(rec->get_field(1)), std::to_string(cur_row));
EXPECT_EQ(field_to_string(rec->get_field(3)),
std::to_string(cur_row * 10));
cur_row++;
}
EXPECT_EQ(cur_row, row_num);
reader.destroy_query_data_set(qds);
ASSERT_EQ(reader.close(), E_OK);
}
// Regression: write_tablet_aligned() used to discard time_write_column_batch
// errors and keep writing value columns. On an out-of-order tablet that left
// the time chunk with fewer rows than the value chunks (or with their seal
// flag still suppressed). The fix propagates the time-column error so no
// value column is touched and the page seal flags are restored.
TEST_F(TsFileWriterTest, AlignedTabletTimeBatchOutOfOrderAborts) {
std::string device_name = "device_aligned_out_of_order";
std::vector<MeasurementSchema> schema_vec;
schema_vec.emplace_back("v0", INT64, PLAIN, UNCOMPRESSED);
schema_vec.emplace_back("v1", INT64, PLAIN, UNCOMPRESSED);
{
std::vector<MeasurementSchema*> reg;
for (auto& s : schema_vec) reg.push_back(new MeasurementSchema(s));
tsfile_writer_->register_aligned_timeseries(device_name, reg);
}
const int row_num = 16;
Tablet tablet(device_name,
std::make_shared<std::vector<MeasurementSchema>>(schema_vec),
row_num);
// Non-monotonic timestamps trip TimePageWriter::write_batch's order check.
for (int i = 0; i < row_num; ++i) {
int64_t ts = (i == row_num - 1) ? 0 : 1000 + i;
ASSERT_EQ(tablet.add_timestamp(i, ts), E_OK);
ASSERT_EQ(tablet.add_value(i, 0u, static_cast<int64_t>(i)), E_OK);
ASSERT_EQ(tablet.add_value(i, 1u, static_cast<int64_t>(i * 2)), E_OK);
}
EXPECT_NE(tsfile_writer_->write_tablet_aligned(tablet), E_OK);
ASSERT_EQ(tsfile_writer_->close(), E_OK);
}
// Regression: write_record_aligned used to ignore the time write return
// value, then unconditionally write each value column. An out-of-order
// timestamp would leave the time chunk one row short of every value chunk
// for the rest of the file. The fix propagates the time-write error and
// marks the writer unrecoverable when value-column writes diverge from
// time.
TEST_F(TsFileWriterTest, RecordAlignedOutOfOrderDoesNotAdvanceValueColumns) {
std::string device_name = "root.dev_aligned_record";
std::vector<MeasurementSchema> schema_vec;
schema_vec.emplace_back("v0", INT64, PLAIN, UNCOMPRESSED);
schema_vec.emplace_back("v1", INT64, PLAIN, UNCOMPRESSED);
{
std::vector<MeasurementSchema*> reg;
for (auto& s : schema_vec) reg.push_back(new MeasurementSchema(s));
tsfile_writer_->register_aligned_timeseries(device_name, reg);
}
// First record at ts=1000 — should write cleanly.
TsRecord r1(1000, device_name);
r1.points_.emplace_back("v0", static_cast<int64_t>(0));
r1.points_.emplace_back("v1", static_cast<int64_t>(0));
ASSERT_EQ(tsfile_writer_->write_record_aligned(r1), E_OK);
// Second record at the same timestamp 1000 — time_chunk_writer rejects
// it (E_OUT_OF_ORDER per TimePageWriter::write). The value columns
// must not advance.
TsRecord r2(1000, device_name);
r2.points_.emplace_back("v0", static_cast<int64_t>(99));
r2.points_.emplace_back("v1", static_cast<int64_t>(99));
EXPECT_EQ(tsfile_writer_->write_record_aligned(r2), E_OUT_OF_ORDER);
// close() must succeed because the failure was caught before any value
// write — writer state is still consistent.
ASSERT_EQ(tsfile_writer_->close(), E_OK);
}
// Regression: the aligned bulk-memcpy fast path in AlignedChunkReader only
// appended bytes to each Vector's value_data without calling add_row_nums().
// Vector::row_num_ stayed at 0 while TsBlock::row_count_ jumped to N, so
// fill_trailling_nulls() then overwrote every just-written row as null
// (visible to the caller as all-null columns).
TEST_F(TsFileWriterTest, AlignedBulkMemcpyAdvancesVectorRowNum) {
std::string device_name = "device_bulk_rownum";
std::vector<MeasurementSchema> schema_vec;
schema_vec.emplace_back("v0", INT64, PLAIN, UNCOMPRESSED);
schema_vec.emplace_back("v1", INT64, PLAIN, UNCOMPRESSED);
{
std::vector<MeasurementSchema*> reg;
for (auto& s : schema_vec) reg.push_back(new MeasurementSchema(s));
tsfile_writer_->register_aligned_timeseries(device_name, reg);
}
const int N = 64;
Tablet tablet(device_name,
std::make_shared<std::vector<MeasurementSchema>>(schema_vec),
N);
for (int i = 0; i < N; i++) {
ASSERT_EQ(tablet.add_timestamp(i, 1000 + i), E_OK);
ASSERT_EQ(tablet.add_value(i, 0u, static_cast<int64_t>(i)), E_OK);
ASSERT_EQ(tablet.add_value(i, 1u, static_cast<int64_t>(i * 2)), E_OK);
}
ASSERT_EQ(tsfile_writer_->write_tablet_aligned(tablet), E_OK);
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
ASSERT_EQ(tsfile_writer_->close(), E_OK);
// Read back via TsBlock — confirms the rows are visible. Under the
// bug Vector::row_num_ stayed at 0, fill_trailling_nulls() then
// marked every just-written row null; the iterator still reports
// them as rows so we check the non-null field for a real value.
std::vector<storage::Path> select;
std::string s0("v0"), s1("v1");
select.emplace_back(device_name, s0);
select.emplace_back(device_name, s1);
storage::QueryExpression* qe =
storage::QueryExpression::create(select, nullptr);
storage::TsFileReader reader;
ASSERT_EQ(reader.open(file_name_), E_OK);
storage::ResultSet* tmp = nullptr;
ASSERT_EQ(reader.query(qe, tmp), E_OK);
auto* qds = (QDSWithoutTimeGenerator*)tmp;
int got = 0;
bool has_next = false;
while (IS_SUCC(qds->next(has_next)) && has_next) {
auto* rec = qds->get_row_record();
ASSERT_NE(rec, nullptr);
got++;
}
EXPECT_EQ(got, N);
reader.destroy_query_data_set(qds);
reader.close();
}
TEST_F(TsFileWriterTest, WriteAlignedMultiFlush) {
int measurement_num = 100, row_num = 100;
std::string device_name = "device";
std::vector<std::string> measurement_names;
for (int i = 0; i < measurement_num; i++) {
measurement_names.emplace_back("temperature" + std::to_string(i));
}
common::TSDataType data_type = common::TSDataType::INT32;
common::TSEncoding encoding = common::TSEncoding::PLAIN;
common::CompressionType compression_type =
common::CompressionType::UNCOMPRESSED;
std::vector<MeasurementSchema*> measurement_schema_vec;
for (const auto& measurement_name : measurement_names) {
auto* ms = new MeasurementSchema(measurement_name, data_type, encoding,
compression_type);
measurement_schema_vec.push_back(ms);
}
tsfile_writer_->register_aligned_timeseries(device_name,
measurement_schema_vec);
for (int i = 0; i < row_num; ++i) {
TsRecord record(1622505600000 + i * 1000, device_name);
for (const auto& measurement_name : measurement_names) {
record.add_point(measurement_name, (int32_t)i);
}
ASSERT_EQ(tsfile_writer_->write_record_aligned(record), E_OK);
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
}
ASSERT_EQ(tsfile_writer_->close(), E_OK);
std::vector<storage::Path> select_list;
for (int i = 0; i < measurement_num; ++i) {
std::string measurement_name = "temperature" + std::to_string(i);
storage::Path path(device_name, measurement_name);
select_list.push_back(path);
}
storage::QueryExpression* query_expr =
storage::QueryExpression::create(select_list, nullptr);
storage::TsFileReader reader;
int ret = reader.open(file_name_);
ASSERT_EQ(ret, common::E_OK);
storage::ResultSet* tmp_qds = nullptr;
ret = reader.query(query_expr, tmp_qds);
auto* qds = (QDSWithoutTimeGenerator*)tmp_qds;
storage::RowRecord* record;
bool has_next = false;
for (int cur_row = 0; cur_row < row_num; cur_row++) {
if (IS_FAIL(qds->next(has_next)) || !has_next) {
break;
}
record = qds->get_row_record();
int size = record->get_fields()->size();
for (int i = 0; i < size; ++i) {
if (i == 0) {
EXPECT_EQ(std::to_string(record->get_timestamp()),
field_to_string(record->get_field(i)));
continue;
}
EXPECT_EQ(std::to_string(cur_row),
field_to_string(record->get_field(i)));
}
}
reader.destroy_query_data_set(qds);
}
TEST_F(TsFileWriterTest, WriteAlignedPartialData) {
int measurement_num = 100, row_num = 200;
std::string device_name = "device";
std::vector<std::string> measurement_names;
for (int i = 0; i < measurement_num; i++) {
measurement_names.emplace_back("temperature" + std::to_string(i));
}
common::TSDataType data_type = common::TSDataType::INT32;
common::TSEncoding encoding = common::TSEncoding::PLAIN;
common::CompressionType compression_type =
common::CompressionType::UNCOMPRESSED;
std::vector<MeasurementSchema*> measurement_schema_vec;
for (const auto& measurement_name : measurement_names) {
auto* ms = new MeasurementSchema(measurement_name, data_type, encoding,
compression_type);
measurement_schema_vec.push_back(ms);
}
tsfile_writer_->register_aligned_timeseries(device_name,
measurement_schema_vec);
for (int i = 0; i < row_num; ++i) {
TsRecord record(1622505600000 + i * 1000, device_name);
for (const auto& measurement_name : measurement_names) {
record.add_point(measurement_name, (int32_t)i);
}
ASSERT_EQ(tsfile_writer_->write_record_aligned(record), E_OK);
}
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
ASSERT_EQ(tsfile_writer_->close(), E_OK);
std::vector<storage::Path> select_list;
for (int i = 0; i < measurement_num; ++i) {
std::string measurement_name = "temperature" + std::to_string(i);
storage::Path path(device_name, measurement_name);
select_list.push_back(path);
}
storage::QueryExpression* query_expr =
storage::QueryExpression::create(select_list, nullptr);
storage::TsFileReader reader;
int ret = reader.open(file_name_);
ASSERT_EQ(ret, common::E_OK);
storage::ResultSet* tmp_qds = nullptr;
ret = reader.query(query_expr, tmp_qds);
auto* qds = (QDSWithoutTimeGenerator*)tmp_qds;
storage::RowRecord* record;
int64_t cur_row = 0;
bool has_next = false;
do {
if (IS_FAIL(qds->next(has_next)) || !has_next) {
break;
}
record = qds->get_row_record();
int size = record->get_fields()->size();
for (int i = 0; i < size; ++i) {
if (i == 0) {
EXPECT_EQ(std::to_string(record->get_timestamp()),
field_to_string(record->get_field(i)));
continue;
}
EXPECT_EQ(std::to_string(cur_row),
field_to_string(record->get_field(i)));
}
cur_row++;
} while (true);
reader.destroy_query_data_set(qds);
}
TEST_F(TsFileWriterTest, WriteTabletDataTypeMismatch) {
for (int i = 0; i < 2; i++) {
std::string device_name = "test_device" + std::to_string(i);
for (int j = 0; j < 3; j++) {
std::string measure_name = "measurement" + std::to_string(j);
tsfile_writer_->register_timeseries(
device_name, storage::MeasurementSchema(
measure_name, common::TSDataType::INT32,
common::TSEncoding::PLAIN,
common::CompressionType::UNCOMPRESSED));
}
}
std::vector<TSDataType> measurement_types{
TSDataType::INT32, TSDataType::INT64, TSDataType::INT32};
std::vector<std::string> measurement_names{"measurement0", "measurement1",
"measurement2"};
Tablet tablet("test_device0", &measurement_names, &measurement_types);
for (int row = 0; row < 100; row++) {
tablet.add_timestamp(row, row);
for (int col = 0; col < 3; col++) {
switch (measurement_types[col]) {
case TSDataType::INT32:
tablet.add_value(row, col, static_cast<int32_t>(row));
break;
case TSDataType::INT64:
tablet.add_value(row, col, static_cast<int64_t>(row));
break;
default:;
}
}
}
ASSERT_EQ(E_TYPE_NOT_MATCH, tsfile_writer_->write_tablet(tablet));
std::vector<MeasurementSchema*> measurement_schemas;
for (int i = 0; i < 3; i++) {
measurement_schemas.push_back(new MeasurementSchema(
"measurement" + std::to_string(i), TSDataType::INT32));
}
tsfile_writer_->register_aligned_timeseries("device3", measurement_schemas);
tablet.set_table_name("device3");
ASSERT_EQ(E_TYPE_NOT_MATCH, tsfile_writer_->write_tablet_aligned(tablet));
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
ASSERT_EQ(tsfile_writer_->close(), E_OK);
}
// Regression: partial-write failures (parallel aligned task failing mid-way,
// non-aligned column failing after earlier columns advanced, etc.) leave per-
// column chunk writers out of sync. The writer latches unrecoverable_ so
// subsequent flush/close/write must refuse rather than seal a corrupt file
// whose time and value chunks disagree on row count. Directly triggering
// the partial failure deterministically is hard, so this test asserts the
// downstream contract by flipping the flag through a friend hook.
namespace storage {
class TsFileWriterUnrecoverableTest {
public:
static void mark_unrecoverable(TsFileWriter& w) { w.unrecoverable_ = true; }
};
} // namespace storage
TEST_F(TsFileWriterTest, UnrecoverableLatchRefusesFlushCloseAndWrites) {
const std::string device = "root.dev_unrec";
std::vector<MeasurementSchema*> reg;
reg.push_back(new MeasurementSchema("v0", INT64, PLAIN, UNCOMPRESSED));
reg.push_back(new MeasurementSchema("v1", INT64, PLAIN, UNCOMPRESSED));
ASSERT_EQ(tsfile_writer_->register_aligned_timeseries(device, reg), E_OK);
// Write one good row so a flush attempt would otherwise have data to emit.
TsRecord r(1000, device);
r.points_.emplace_back("v0", static_cast<int64_t>(0));
r.points_.emplace_back("v1", static_cast<int64_t>(0));
ASSERT_EQ(tsfile_writer_->write_record_aligned(r), E_OK);
// Simulate the post-partial-failure state.
storage::TsFileWriterUnrecoverableTest::mark_unrecoverable(*tsfile_writer_);
// Every public write/flush/close entry point must refuse.
EXPECT_EQ(tsfile_writer_->flush(), E_DATA_INCONSISTENCY);
EXPECT_EQ(tsfile_writer_->close(), E_DATA_INCONSISTENCY);
TsRecord r2(1001, device);
r2.points_.emplace_back("v0", static_cast<int64_t>(1));
r2.points_.emplace_back("v1", static_cast<int64_t>(1));
EXPECT_EQ(tsfile_writer_->write_record_aligned(r2), E_DATA_INCONSISTENCY);
Tablet tablet(device,
std::make_shared<std::vector<MeasurementSchema>>(
std::vector<MeasurementSchema>{
MeasurementSchema("v0", INT64, PLAIN, UNCOMPRESSED),
MeasurementSchema("v1", INT64, PLAIN, UNCOMPRESSED)}),
4);
for (int i = 0; i < 4; i++) {
ASSERT_EQ(tablet.add_timestamp(i, 2000 + i), E_OK);
ASSERT_EQ(tablet.add_value(i, 0u, static_cast<int64_t>(i)), E_OK);
ASSERT_EQ(tablet.add_value(i, 1u, static_cast<int64_t>(i * 2)), E_OK);
}
EXPECT_EQ(tsfile_writer_->write_tablet_aligned(tablet),
E_DATA_INCONSISTENCY);
EXPECT_EQ(tsfile_writer_->write_tablet(tablet), E_DATA_INCONSISTENCY);
}
namespace {
WriteFile* OpenWriteFileFor(const std::string& path) {
int flags = O_WRONLY | O_CREAT | O_TRUNC;
#ifdef _WIN32
flags |= O_BINARY;
#endif
auto* wf = new WriteFile;
if (wf->create(path, flags, 0666) != E_OK) {
delete wf;
return nullptr;
}
return wf;
}
void WriteOneAlignedRow(TsFileWriter& w, const std::string& device, int64_t ts,
int64_t value) {
std::vector<MeasurementSchema*> reg;
reg.push_back(new MeasurementSchema("v0", INT64, PLAIN, UNCOMPRESSED));
ASSERT_EQ(w.register_aligned_timeseries(device, reg), E_OK);
TsRecord r(ts, device);
r.points_.emplace_back("v0", value);
ASSERT_EQ(w.write_record_aligned(r), E_OK);
}
} // namespace
// Writing speed up: TsFileWriter must be reusable across a
// destroy() + init() cycle.
// - 1: TsFileIOWriter::destroy() left chunk_group_meta_list_ and
// chunk_group_meta_index_ pointing at meta_allocator_-owned memory that
// the next init() then re-armed; the next start_flush_chunk_group()
// linear scan would deref freed nodes.
// - 2: TsFileWriter::init() did not reset start_file_done_, so
// the second file's flush() skipped the magic/version header and
// produced a file the reader can't open.
// This test forces both code paths: destroy(), init() onto a fresh
// WriteFile, write data, close, then read the second file via the public
// TsFileReader API.
TEST_F(TsFileWriterTest, WriterReuseAfterDestroyProducesValidSecondFile) {
// First lifecycle uses the fixture-provided writer (already open()'d on
// file_name_). Write one row and close — this flushes the magic +
// version into file_name_ and flips start_file_done_ true.
WriteOneAlignedRow(*tsfile_writer_, "root.dev_first", 1000, 7);
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
ASSERT_EQ(tsfile_writer_->close(), E_OK);
// Second lifecycle: tear down the previous writer state and re-init
// against a brand-new file.
tsfile_writer_->destroy();
const std::string second_path = std::string("tsfile_writer_reuse_test_") +
generate_random_string(10) +
std::string(".tsfile");
remove(second_path.c_str());
WriteFile* wf = OpenWriteFileFor(second_path);
ASSERT_NE(wf, nullptr);
ASSERT_EQ(tsfile_writer_->init(wf), E_OK);
WriteOneAlignedRow(*tsfile_writer_, "root.dev_second", 2000, 9);
ASSERT_EQ(tsfile_writer_->flush(), E_OK);
ASSERT_EQ(tsfile_writer_->close(), E_OK);
// The second file must start with the TsFile magic + version byte.
// The TsFileReader open path mostly indexes from the file tail, so a
// missing magic at offset 0 isn't caught by reader.open(). Inspect the
// raw header bytes instead — that's exactly what start_file_done_ guards.
{
std::ifstream in(second_path, std::ios::binary);
ASSERT_TRUE(in.is_open());
char header[MAGIC_STRING_TSFILE_LEN + 1] = {0};
in.read(header, MAGIC_STRING_TSFILE_LEN + 1);
EXPECT_EQ(in.gcount(),
static_cast<std::streamsize>(MAGIC_STRING_TSFILE_LEN + 1));
EXPECT_EQ(memcmp(header, MAGIC_STRING_TSFILE, MAGIC_STRING_TSFILE_LEN),
0)
<< "second-file header is missing the TsFile magic — "
"start_file_done_ residual from the previous lifecycle";
EXPECT_EQ(header[MAGIC_STRING_TSFILE_LEN], VERSION_NUM_BYTE);
}
// wf was passed to init() but init() did not take ownership.
delete wf;
remove(second_path.c_str());
}