diff --git a/.github/workflows/cppcheck.yml b/.github/workflows/cppcheck.yml
new file mode 100644
index 000000000..5676a965c
--- /dev/null
+++ b/.github/workflows/cppcheck.yml
@@ -0,0 +1,167 @@
+# 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 at
+#
+# 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.
+
+name: Cppcheck
+
+on:
+ push:
+ branches:
+ - develop
+ - iotdb
+ - rc/*
+ paths:
+ - '.github/workflows/cppcheck.yml'
+ - '.mvn/**'
+ - 'cpp/**'
+ - 'mvnw'
+ - 'pom.xml'
+ pull_request:
+ branches:
+ - develop
+ - dev/*
+ - iotdb
+ - rc/*
+ paths:
+ - '.github/workflows/cppcheck.yml'
+ - '.mvn/**'
+ - 'cpp/**'
+ - 'mvnw'
+ - 'pom.xml'
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+env:
+ CPPCHECK_VERSION: 2.17.1
+ CPPCHECK_SOURCE_SHA256: bfd681868248ec03855ca7c2aea7bcb1f39b8b18860d76aec805a92a967b966c
+ MAVEN_OPTS: -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.http.retryHandler.class=standard -Dmaven.wagon.http.retryHandler.count=3
+ DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
+
+jobs:
+ cppcheck:
+ runs-on: ubuntu-24.04
+ timeout-minutes: 30
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v7
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v5.6.0
+ with:
+ distribution: corretto
+ java-version: 17
+
+ - name: Cache Maven packages
+ uses: actions/cache@v6
+ with:
+ path: ~/.m2
+ key: ${{ runner.os }}-m2-cppcheck-${{ hashFiles('**/pom.xml') }}
+ restore-keys: ${{ runner.os }}-m2-
+
+ - name: Cache Cppcheck
+ uses: actions/cache@v6
+ with:
+ path: ~/.cache/cppcheck/${{ env.CPPCHECK_VERSION }}
+ key: cppcheck-${{ runner.os }}-${{ runner.arch }}-${{ env.CPPCHECK_VERSION }}-${{ env.CPPCHECK_SOURCE_SHA256 }}
+
+ - name: Install dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y uuid-dev
+
+ - name: Install Cppcheck
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ cppcheck_home="${HOME}/.cache/cppcheck/${CPPCHECK_VERSION}"
+ if [[ ! -x "${cppcheck_home}/bin/cppcheck" ]]; then
+ build_root="$(mktemp -d)"
+ archive="${build_root}/cppcheck.tar.gz"
+ source_dir="${build_root}/source"
+ build_dir="${build_root}/build"
+
+ mkdir -p "${source_dir}" "${build_dir}" "${cppcheck_home}"
+ curl --fail --location --retry 3 \
+ "https://github.com/cppcheck-opensource/cppcheck/archive/refs/tags/${CPPCHECK_VERSION}.tar.gz" \
+ --output "${archive}"
+ echo "${CPPCHECK_SOURCE_SHA256} ${archive}" | sha256sum --check -
+ tar -xzf "${archive}" --strip-components=1 -C "${source_dir}"
+
+ cmake -S "${source_dir}" -B "${build_dir}" \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DUSE_MATCHCOMPILER=ON \
+ -DBUILD_GUI=OFF \
+ -DBUILD_TESTS=OFF \
+ -DCMAKE_INSTALL_PREFIX="${cppcheck_home}"
+ cmake --build "${build_dir}" --parallel "$(nproc)"
+ cmake --install "${build_dir}"
+ fi
+
+ echo "${cppcheck_home}/bin" >> "${GITHUB_PATH}"
+ "${cppcheck_home}/bin/cppcheck" --version
+
+ - name: Generate compilation database with Maven
+ run: |
+ ./mvnw -P with-cpp,with-cppcheck -pl cpp \
+ -Dbuild.test=OFF \
+ cmake:generate@cmake-generate-test-compile
+ test -s cpp/target/build/compile_commands.json
+
+ - name: Run Cppcheck
+ shell: bash
+ run: |
+ mkdir -p cpp/target/cppcheck
+
+ set +e
+ cppcheck \
+ --project=cpp/target/build/compile_commands.json \
+ --file-filter='*cpp/src/*' \
+ --file-filter='*cpp/tools/*' \
+ --file-filter='*cpp/examples/*' \
+ --enable=warning,performance,portability \
+ --check-level=exhaustive \
+ --inline-suppr \
+ --suppress=missingIncludeSystem \
+ --suppress=unusedFunction \
+ --suppress='*:*/third_party/*' \
+ --suppress='*:*/target/*' \
+ --suppress='*:*/parser/generated/*' \
+ --error-exitcode=2 \
+ --template=gcc \
+ --output-file=cpp/target/cppcheck/cppcheck.txt \
+ --quiet \
+ -j "$(nproc)"
+ cppcheck_status=$?
+ set -e
+
+ cat cpp/target/cppcheck/cppcheck.txt
+ exit "${cppcheck_status}"
+
+ - name: Upload Cppcheck report
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: cppcheck-report
+ path: cpp/target/cppcheck/cppcheck.txt
+ if-no-files-found: ignore
diff --git a/cpp/examples/c_examples/demo_write.c b/cpp/examples/c_examples/demo_write.c
index cf5ffea28..7c795452e 100644
--- a/cpp/examples/c_examples/demo_write.c
+++ b/cpp/examples/c_examples/demo_write.c
@@ -33,25 +33,69 @@ static char* duplicate_string(const char* src) {
return dst;
}
+static ERRNO cleanup_write_tsfile_resources(WriteFile* file,
+ TsFileWriter writer, Tablet* tablet,
+ TableSchema* table_schema,
+ ERRNO code) {
+ if (*tablet != NULL) {
+ free_tablet(tablet);
+ }
+
+ if (writer != NULL) {
+ ERRNO close_code = tsfile_writer_close(writer);
+ if (code == RET_OK) {
+ code = close_code;
+ }
+ }
+
+ if (table_schema->table_name != NULL ||
+ table_schema->column_schemas != NULL) {
+ free_table_schema(*table_schema);
+ }
+
+ if (*file != NULL) {
+ free_write_file(file);
+ }
+
+ if (code != RET_OK) {
+ printf("get err no: %d", code);
+ }
+ return code;
+}
+
// This example shows you how to write tsfile.
ERRNO write_tsfile() {
ERRNO code = 0;
+ WriteFile file = NULL;
+ TsFileWriter writer = NULL;
+ Tablet tablet = NULL;
+ TableSchema table_schema = {0};
+
code = set_global_compression(TS_COMPRESSION_LZ4);
if (code != RET_OK) {
- return code;
+ return cleanup_write_tsfile_resources(&file, writer, &tablet,
+ &table_schema, code);
}
code = set_datatype_encoding(TS_DATATYPE_INT32, TS_ENCODING_TS_2DIFF);
if (code != RET_OK) {
- return code;
+ return cleanup_write_tsfile_resources(&file, writer, &tablet,
+ &table_schema, code);
}
char* table_name = "table1";
// Create table schema to describe a table in a tsfile.
- TableSchema table_schema;
table_schema.table_name = duplicate_string(table_name);
- table_schema.column_num = 3;
+ if (table_schema.table_name == NULL) {
+ return cleanup_write_tsfile_resources(&file, writer, &tablet,
+ &table_schema, RET_OOM);
+ }
table_schema.column_schemas =
- (ColumnSchema*)malloc(sizeof(ColumnSchema) * 3);
+ (ColumnSchema*)calloc(3, sizeof(ColumnSchema));
+ if (table_schema.column_schemas == NULL) {
+ return cleanup_write_tsfile_resources(&file, writer, &tablet,
+ &table_schema, RET_OOM);
+ }
+ table_schema.column_num = 3;
table_schema.column_schemas[0] =
(ColumnSchema){.column_name = duplicate_string("id1"),
.data_type = TS_DATATYPE_STRING,
@@ -64,22 +108,37 @@ ERRNO write_tsfile() {
(ColumnSchema){.column_name = duplicate_string("s1"),
.data_type = TS_DATATYPE_INT32,
.column_category = FIELD};
+ if (table_schema.column_schemas[0].column_name == NULL ||
+ table_schema.column_schemas[1].column_name == NULL ||
+ table_schema.column_schemas[2].column_name == NULL) {
+ return cleanup_write_tsfile_resources(&file, writer, &tablet,
+ &table_schema, RET_OOM);
+ }
remove("test_c.tsfile");
// Create a file with specify path to write tsfile.
- WriteFile file = write_file_new("test_c.tsfile", &code);
- HANDLE_ERROR(code);
+ file = write_file_new("test_c.tsfile", &code);
+ if (code != RET_OK) {
+ return cleanup_write_tsfile_resources(&file, writer, &tablet,
+ &table_schema, code);
+ }
// Create tsfile writer with specify table schema.
- TsFileWriter writer = tsfile_writer_new(file, &table_schema, &code);
- HANDLE_ERROR(code);
+ writer = tsfile_writer_new(file, &table_schema, &code);
+ if (code != RET_OK) {
+ return cleanup_write_tsfile_resources(&file, writer, &tablet,
+ &table_schema, code);
+ }
// Create tablet to insert data.
- Tablet tablet =
- tablet_new((char*[]){"id1", "id2", "s1"},
- (TSDataType[]){TS_DATATYPE_STRING, TS_DATATYPE_STRING,
- TS_DATATYPE_INT32},
- 3, 5);
+ tablet = tablet_new((char*[]){"id1", "id2", "s1"},
+ (TSDataType[]){TS_DATATYPE_STRING, TS_DATATYPE_STRING,
+ TS_DATATYPE_INT32},
+ 3, 5);
+ if (tablet == NULL) {
+ return cleanup_write_tsfile_resources(&file, writer, &tablet,
+ &table_schema, RET_OOM);
+ }
for (int row = 0; row < 5; row++) {
Timestamp timestamp = row;
@@ -92,19 +151,7 @@ ERRNO write_tsfile() {
}
// Write tablet data.
- HANDLE_ERROR(tsfile_writer_write(writer, tablet));
-
- // Free tablet.
- free_tablet(&tablet);
-
- // Free table schema we used before.
- free_table_schema(table_schema);
-
- // Close writer.
- HANDLE_ERROR(tsfile_writer_close(writer));
-
- // Close write file after closing writer.
- free_write_file(&file);
-
- return 0;
+ code = tsfile_writer_write(writer, tablet);
+ return cleanup_write_tsfile_resources(&file, writer, &tablet, &table_schema,
+ code);
}
diff --git a/cpp/pom.xml b/cpp/pom.xml
index 9b47ce0a7..e94debff2 100644
--- a/cpp/pom.xml
+++ b/cpp/pom.xml
@@ -292,6 +292,32 @@
+
+
+ with-cppcheck
+
+
+
+ com.googlecode.cmake-maven-project
+ cmake-maven-plugin
+
+
+ cmake-generate-test-compile
+
+
+
+
+
+
+
+
+
+
+
.java-9-and-above
diff --git a/cpp/src/common/path.h b/cpp/src/common/path.h
index f09b8c316..05f988696 100644
--- a/cpp/src/common/path.h
+++ b/cpp/src/common/path.h
@@ -40,9 +40,8 @@ struct Path {
Path(std::string& device, std::string& measurement)
: measurement_(measurement),
- device_id_(std::make_shared(device)) {
- full_path_ = device + "." + measurement;
- }
+ device_id_(std::make_shared(device)),
+ full_path_(device + "." + measurement) {}
Path(const std::string& path_sc, bool if_split = true) {
if (!path_sc.empty()) {
diff --git a/cpp/src/common/row_record.h b/cpp/src/common/row_record.h
index d6da63efd..a640ecc29 100644
--- a/cpp/src/common/row_record.h
+++ b/cpp/src/common/row_record.h
@@ -188,8 +188,8 @@ FORCE_INLINE Field* make_null_literal() {
class RowRecord {
public:
- explicit RowRecord(uint32_t col_num) : col_num_(col_num) {
- fields_ = new std::vector();
+ explicit RowRecord(uint32_t col_num)
+ : time_(0), col_num_(col_num), fields_(new std::vector()) {
fields_->reserve(col_num);
for (uint32_t i = 0; i < col_num; ++i) {
Field* val = make_null_literal();
@@ -197,8 +197,8 @@ class RowRecord {
}
}
- RowRecord(int64_t time, uint32_t col_num) : time_(time), col_num_(col_num) {
- fields_ = new std::vector();
+ RowRecord(int64_t time, uint32_t col_num)
+ : time_(time), col_num_(col_num), fields_(new std::vector()) {
fields_->reserve(col_num_);
for (uint32_t i = 0; i < col_num_; ++i) {
Field* val = make_null_literal();
@@ -206,6 +206,9 @@ class RowRecord {
}
}
+ RowRecord(const RowRecord&) = delete;
+ RowRecord& operator=(const RowRecord&) = delete;
+
~RowRecord() {
if (fields_) {
int size = fields_->size();
diff --git a/cpp/src/common/tsblock/tsblock.cc b/cpp/src/common/tsblock/tsblock.cc
index 9d69566be..fd29f9f07 100644
--- a/cpp/src/common/tsblock/tsblock.cc
+++ b/cpp/src/common/tsblock/tsblock.cc
@@ -18,6 +18,7 @@
*/
#include "tsblock.h"
+#include
#include
namespace common {
@@ -73,29 +74,32 @@ void TsBlock::write_data(ByteStream* __restrict byte_stream,
std::string strval;
switch (type) {
case common::INT64: {
- int64_t ival = *reinterpret_cast(val);
+ int64_t ival;
+ std::memcpy(&ival, val, sizeof(ival));
strval = std::to_string(ival);
break;
}
case common::INT32: {
- int32_t ival = *reinterpret_cast(val);
+ int32_t ival;
+ std::memcpy(&ival, val, sizeof(ival));
strval = std::to_string(ival);
break;
}
case common::FLOAT: {
- float ival = *reinterpret_cast(
- val); // cppcheck-suppress invalidPointerCast
+ float ival;
+ std::memcpy(&ival, val, sizeof(ival));
strval = std::to_string(ival);
break;
}
case common::DOUBLE: {
- double ival = *reinterpret_cast(
- val); // cppcheck-suppress invalidPointerCast
+ double ival;
+ std::memcpy(&ival, val, sizeof(ival));
strval = std::to_string(ival);
break;
}
case common::BOOLEAN: {
- bool ival = *reinterpret_cast(val);
+ bool ival;
+ std::memcpy(&ival, val, sizeof(ival));
if (ival) {
strval = "true";
} else {
diff --git a/cpp/src/common/tsblock/tsblock.h b/cpp/src/common/tsblock/tsblock.h
index b68af1611..7959f7c36 100644
--- a/cpp/src/common/tsblock/tsblock.h
+++ b/cpp/src/common/tsblock/tsblock.h
@@ -21,6 +21,8 @@
#include
+#include
+
#include "common/allocator/byte_stream.h"
#include "common/container/byte_buffer.h"
#include "common/global.h"
@@ -158,11 +160,13 @@ class RowAppender {
// TODO(Colin): Refine this.
TSDataType datatype = vec->get_vector_type();
if (len == 4 && datatype == INT64) {
- int32_t int32_val = *reinterpret_cast(value);
+ int32_t int32_val;
+ std::memcpy(&int32_val, value, sizeof(int32_val));
int64_t int64_val = static_cast(int32_val);
vec->append(reinterpret_cast(&int64_val), 8);
} else if (len == 4 && datatype == DOUBLE) {
- float float_val = *reinterpret_cast(value);
+ float float_val;
+ std::memcpy(&float_val, value, sizeof(float_val));
double double_val = static_cast(float_val);
vec->append(reinterpret_cast(&double_val), 8);
} else {
diff --git a/cpp/src/common/tsblock/tuple_desc.h b/cpp/src/common/tsblock/tuple_desc.h
index 981d42385..39dce6273 100644
--- a/cpp/src/common/tsblock/tuple_desc.h
+++ b/cpp/src/common/tsblock/tuple_desc.h
@@ -62,7 +62,7 @@ class TupleDesc {
time_column_index_ = -1;
}
- FORCE_INLINE void push_back(ColumnSchema schema) {
+ FORCE_INLINE void push_back(const ColumnSchema& schema) {
if (schema.column_category_ == ColumnCategory::TIME) {
ASSERT(time_column_index_ == -1);
time_column_index_ = static_cast(column_list_.size());
@@ -97,7 +97,7 @@ class TupleDesc {
return time_column_index_;
}
- FORCE_INLINE std::string get_column_name(uint32_t index) {
+ FORCE_INLINE const std::string& get_column_name(uint32_t index) const {
return column_list_[index].column_name_;
}
diff --git a/cpp/src/compress/gzip_compressor.cc b/cpp/src/compress/gzip_compressor.cc
index b49830bf2..49e7ac566 100644
--- a/cpp/src/compress/gzip_compressor.cc
+++ b/cpp/src/compress/gzip_compressor.cc
@@ -23,7 +23,8 @@ using namespace common;
namespace storage {
-GzipCompressor::GzipCompressor() : compressed_buf() { zstream_valid_ = false; }
+GzipCompressor::GzipCompressor()
+ : compress_stream_(), compressed_buf(), zstream_valid_(false) {}
GzipCompressor::~GzipCompressor() { end_zstream(); }
@@ -140,9 +141,8 @@ int GzipCompressor::compress(char* uncompressed_buf,
return ret;
}
-GzipDeCompressor::GzipDeCompressor() : decompressed_buf() {
- zstream_valid_ = false;
-}
+GzipDeCompressor::GzipDeCompressor()
+ : decompress_stream_(), decompressed_buf(), zstream_valid_(false) {}
GzipDeCompressor::~GzipDeCompressor() { end_zstream(); }
diff --git a/cpp/src/file/write_file.h b/cpp/src/file/write_file.h
index 014fa1ae6..84358dcdf 100644
--- a/cpp/src/file/write_file.h
+++ b/cpp/src/file/write_file.h
@@ -46,7 +46,7 @@ class WriteFile {
int truncate(int64_t size);
/** Seek to end of file. Used after open to position for append. */
int seek_to_end();
- FORCE_INLINE std::string get_file_path() { return path_; }
+ FORCE_INLINE const std::string& get_file_path() const { return path_; }
/** Current file offset. After seek_to_end(), equals file size (for
* recovery). */
int64_t get_position();
diff --git a/cpp/src/parser/path_visitor.cpp b/cpp/src/parser/path_visitor.cpp
index 6c7968410..96b6c9478 100644
--- a/cpp/src/parser/path_visitor.cpp
+++ b/cpp/src/parser/path_visitor.cpp
@@ -103,7 +103,8 @@ bool PathVisitor::ends_with(const std::string& str, const std::string& suffix) {
bool PathVisitor::is_creatable(const std::string& str) {
try {
- std::stod(str);
+ const double parsed_value = std::stod(str);
+ (void)parsed_value;
return true;
} catch (const std::invalid_argument& e) {
return false;
diff --git a/cpp/src/reader/expression.cc b/cpp/src/reader/expression.cc
index 06a615e15..08ffac98e 100644
--- a/cpp/src/reader/expression.cc
+++ b/cpp/src/reader/expression.cc
@@ -108,8 +108,9 @@ Expression* QueryExpression::merge_second_tree_to_first_tree(
Expression* QueryExpression::push_global_time_filter_to_all_series(
Expression* time_filter, std::vector& selected_series) {
- if (selected_series.size() == 0) {
+ if (selected_series.empty()) {
std::cout << "size of selectSeries could not be 0" << std::endl;
+ return nullptr;
}
Expression* expression = new Expression(SERIES_EXPR, selected_series.at(0),
@@ -129,6 +130,9 @@ Expression* QueryExpression::handle_one_global_time_filter(
Expression* left, Expression* expression,
std::vector& selected_series, ExpressionType type) {
Expression* expr = optimize(expression, selected_series);
+ if (expr == nullptr) {
+ return nullptr;
+ }
if (expr->type_ == GLOBALTIME_EXPR) {
return combine_two_global_time_filter(left, expr, type);
@@ -140,6 +144,9 @@ Expression* QueryExpression::handle_one_global_time_filter(
} else if (type == OR_EXPR) {
Expression* after_transform =
push_global_time_filter_to_all_series(left, selected_series);
+ if (after_transform == nullptr) {
+ return nullptr;
+ }
return merge_second_tree_to_first_tree(after_transform, expr);
}
std::cout << "unknown relation in Expression:" << type << std::endl;
@@ -168,6 +175,9 @@ Expression* QueryExpression::optimize(Expression* expression,
right->type_ != GLOBALTIME_EXPR) {
Expression* regular_left = optimize(left, series_paths);
Expression* regular_right = optimize(right, series_paths);
+ if (regular_left == nullptr || regular_right == nullptr) {
+ return nullptr;
+ }
Expression* mid_ret = nullptr;
if (type == AND_EXPR) {
mid_ret = new Expression(AND_EXPR, regular_left, regular_right);
diff --git a/cpp/src/reader/filter/binary_filter.h b/cpp/src/reader/filter/binary_filter.h
index 4cacd52a1..586e94f6a 100644
--- a/cpp/src/reader/filter/binary_filter.h
+++ b/cpp/src/reader/filter/binary_filter.h
@@ -25,7 +25,7 @@
namespace storage {
class BinaryFilter : public Filter {
public:
- BinaryFilter() : Filter() {}
+ BinaryFilter() : Filter(), left_(nullptr), right_(nullptr) {}
BinaryFilter(Filter* left, Filter* right)
: Filter(), left_(left), right_(right) {}
virtual ~BinaryFilter() {}
diff --git a/cpp/src/reader/table_result_set.cc b/cpp/src/reader/table_result_set.cc
index 6de093d24..1a8d2a687 100644
--- a/cpp/src/reader/table_result_set.cc
+++ b/cpp/src/reader/table_result_set.cc
@@ -20,6 +20,8 @@
#include
+#include
+
namespace storage {
void TableResultSet::init() {
row_record_ = new RowRecord(column_names_.size() + 1);
@@ -127,14 +129,16 @@ bool TableResultSet::is_null(uint32_t column_index) {
// the column's physical storage width (DATE is int32, not int64). On a
// mismatch it fires in debug instead of silently splicing the adjacent cell's
// bytes into the result.
-#define TSFILE_FAST_PRIMITIVE_READ(TYPE, DFLT) \
- if (!row_ready_) return DFLT; \
- common::Vector* vec = row_iterator_->get_vector(column_index - 1); \
- ASSERT(common::TypeMatch(vec->get_vector_type())); \
- if (vec->has_null() && vec->is_null(row_iterator_->get_row_id())) \
- return DFLT; \
- return *reinterpret_cast(vec->get_value_data().get_data() + \
- vec->get_offset())
+#define TSFILE_FAST_PRIMITIVE_READ(TYPE, DFLT) \
+ if (!row_ready_) return DFLT; \
+ common::Vector* vec = row_iterator_->get_vector(column_index - 1); \
+ ASSERT(common::TypeMatch(vec->get_vector_type())); \
+ if (vec->has_null() && vec->is_null(row_iterator_->get_row_id())) \
+ return DFLT; \
+ TYPE value; \
+ std::memcpy(&value, vec->get_value_data().get_data() + vec->get_offset(), \
+ sizeof(value)); \
+ return value
bool TableResultSet::get_bool_at(uint32_t column_index) {
TSFILE_FAST_PRIMITIVE_READ(bool, false);
diff --git a/cpp/test/common/row_record_test.cc b/cpp/test/common/row_record_test.cc
index 6b8b54a15..a88d7e2b9 100644
--- a/cpp/test/common/row_record_test.cc
+++ b/cpp/test/common/row_record_test.cc
@@ -104,6 +104,7 @@ TEST(FieldTest, MakeNullLiteral) {
TEST(RowRecordTest, ConstructorWithColNum) {
RowRecord row_record(5);
+ EXPECT_EQ(row_record.get_timestamp(), 0);
EXPECT_EQ(row_record.get_fields()->size(), 5);
for (Field* field : *row_record.get_fields()) {
EXPECT_EQ(field->type_, common::NULL_TYPE);
diff --git a/cpp/test/reader/expression_test.cc b/cpp/test/reader/expression_test.cc
new file mode 100644
index 000000000..f4f9fbacc
--- /dev/null
+++ b/cpp/test/reader/expression_test.cc
@@ -0,0 +1,40 @@
+/*
+ * 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 at
+ *
+ * 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 "reader/expression.h"
+
+#include
+
+#include
+
+namespace storage {
+
+TEST(QueryExpressionTest, EmptySelectedSeriesReturnsNull) {
+ QueryExpression query_expression;
+ std::vector selected_series;
+ Expression* expression = new Expression(
+ OR_EXPR, new Expression(GLOBALTIME_EXPR, static_cast(nullptr)),
+ new Expression(SERIES_EXPR, Path(), static_cast(nullptr)));
+
+ EXPECT_EQ(query_expression.optimize(expression, selected_series), nullptr);
+
+ delete expression;
+}
+
+} // namespace storage