Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions cpp/src/cwrapper/tsfile_cwrapper.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1118,8 +1118,13 @@ int duplicate_ideviceid_to_device_fields(storage::IDeviceID* id,
for (int i = 0; i < n; i++) {
const std::string* ps =
(static_cast<size_t>(i) < segs.size()) ? segs[i] : nullptr;
const char* lit = (ps != nullptr) ? ps->c_str() : "null";
seg_arr[i] = strdup(lit);
// A null tag segment is exposed as a NULL pointer so callers can
// distinguish a missing/null tag from the literal string "null".
if (ps == nullptr) {
seg_arr[i] = nullptr;
continue;
}
seg_arr[i] = strdup(ps->c_str());
if (seg_arr[i] == nullptr) {
for (int j = 0; j < i; j++) {
free(seg_arr[j]);
Expand Down Expand Up @@ -1627,6 +1632,12 @@ TagFilterHandle tsfile_tag_filter_create(TsFileReader reader,
case TAG_FILTER_NOT_REGEXP:
filter = builder.not_reg_exp(column_name, value);
break;
case TAG_FILTER_IS_NULL:
filter = builder.is_null(column_name);
break;
case TAG_FILTER_IS_NOT_NULL:
filter = builder.is_not_null(column_name);
break;
default:
*err_code = common::E_INVALID_ARG;
return nullptr;
Expand Down
5 changes: 4 additions & 1 deletion cpp/src/cwrapper/tsfile_cwrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,8 @@ typedef enum {
TAG_FILTER_GTEQ = 5,
TAG_FILTER_REGEXP = 6,
TAG_FILTER_NOT_REGEXP = 7,
TAG_FILTER_IS_NULL = 8,
TAG_FILTER_IS_NOT_NULL = 9,
} TagFilterOp;

/**
Expand All @@ -884,7 +886,8 @@ typedef enum {
* index).
* @param table_name [in] Table name whose schema defines the TAG columns.
* @param column_name [in] Name of the TAG column to filter on.
* @param value [in] Comparison value (string).
* @param value [in] Comparison value (string). Ignored for
* TAG_FILTER_IS_NULL / TAG_FILTER_IS_NOT_NULL (may be NULL).
* @param op [in] Comparison operator (TagFilterOp).
* @param err_code [out] Error code. E_OK(0) on success.
* @return TagFilterHandle on success; NULL on failure.
Expand Down
60 changes: 50 additions & 10 deletions cpp/src/reader/filter/tag_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ TagEq::TagEq(int col_idx, std::string tag_value)
: TagFilter(col_idx, std::move(tag_value)) {}

bool TagEq::satisfyRow(std::vector<std::string*> segments) const {
if (col_idx_ >= segments.size()) return false;
if (col_idx_ >= segments.size() || segments[col_idx_] == nullptr)
return false;
return *segments[col_idx_] == value_;
}

Expand All @@ -53,7 +54,8 @@ TagNeq::TagNeq(int col_idx, std::string tag_value)
: TagFilter(col_idx, std::move(tag_value)) {}

bool TagNeq::satisfyRow(std::vector<std::string*> segments) const {
if (col_idx_ >= segments.size()) return false;
if (col_idx_ >= segments.size() || segments[col_idx_] == nullptr)
return false;
return *segments[col_idx_] != value_;
}

Expand All @@ -62,7 +64,8 @@ TagLt::TagLt(int col_idx, std::string tag_value)
: TagFilter(col_idx, std::move(tag_value)) {}

bool TagLt::satisfyRow(std::vector<std::string*> segments) const {
if (col_idx_ >= segments.size()) return false;
if (col_idx_ >= segments.size() || segments[col_idx_] == nullptr)
return false;
return *segments[col_idx_] < value_;
}

Expand All @@ -71,7 +74,8 @@ TagLteq::TagLteq(int col_idx, std::string tag_value)
: TagFilter(col_idx, std::move(tag_value)) {}

bool TagLteq::satisfyRow(std::vector<std::string*> segments) const {
if (col_idx_ >= segments.size()) return false;
if (col_idx_ >= segments.size() || segments[col_idx_] == nullptr)
return false;
return *segments[col_idx_] <= value_;
}

Expand All @@ -80,7 +84,8 @@ TagGt::TagGt(int col_idx, std::string tag_value)
: TagFilter(col_idx, std::move(tag_value)) {}

bool TagGt::satisfyRow(std::vector<std::string*> segments) const {
if (col_idx_ >= segments.size()) return false;
if (col_idx_ >= segments.size() || segments[col_idx_] == nullptr)
return false;
return *segments[col_idx_] > value_;
}

Expand All @@ -89,7 +94,8 @@ TagGteq::TagGteq(int col_idx, std::string tag_value)
: TagFilter(col_idx, std::move(tag_value)) {}

bool TagGteq::satisfyRow(std::vector<std::string*> segments) const {
if (col_idx_ >= segments.size()) return false;
if (col_idx_ >= segments.size() || segments[col_idx_] == nullptr)
return false;
return *segments[col_idx_] >= value_;
}

Expand All @@ -105,7 +111,9 @@ TagRegExp::TagRegExp(int col_idx, std::string tag_value)
}

bool TagRegExp::satisfyRow(std::vector<std::string*> segments) const {
if (col_idx_ >= segments.size() || !is_valid_pattern_) return false;
if (col_idx_ >= segments.size() || segments[col_idx_] == nullptr ||
!is_valid_pattern_)
return false;
try {
return std::regex_search(*segments[col_idx_], pattern_);
} catch (const std::regex_error&) {
Expand All @@ -125,14 +133,32 @@ TagNotRegExp::TagNotRegExp(int col_idx, std::string tag_value)
}

bool TagNotRegExp::satisfyRow(std::vector<std::string*> segments) const {
if (col_idx_ >= segments.size() || !is_valid_pattern_) return false;
if (col_idx_ >= segments.size() || segments[col_idx_] == nullptr ||
!is_valid_pattern_)
return false;
try {
return !std::regex_search(*segments[col_idx_], pattern_);
} catch (const std::regex_error&) {
return true;
}
}

// TagIsNull implementation
TagIsNull::TagIsNull(int col_idx) : TagFilter(col_idx, "") {}

bool TagIsNull::satisfyRow(std::vector<std::string*> segments) const {
// A tag is null when its segment is an explicit null pointer or when the
// device id omits the (trailing) segment entirely.
return col_idx_ >= segments.size() || segments[col_idx_] == nullptr;
}

// TagIsNotNull implementation
TagIsNotNull::TagIsNotNull(int col_idx) : TagFilter(col_idx, "") {}

bool TagIsNotNull::satisfyRow(std::vector<std::string*> segments) const {
return col_idx_ < segments.size() && segments[col_idx_] != nullptr;
}

// TagBetween implementation
TagBetween::TagBetween(int col_idx, std::string lower_value,
std::string upper_value)
Expand All @@ -141,7 +167,8 @@ TagBetween::TagBetween(int col_idx, std::string lower_value,
}

bool TagBetween::satisfyRow(std::vector<std::string*> segments) const {
if (col_idx_ >= segments.size()) return false;
if (col_idx_ >= segments.size() || segments[col_idx_] == nullptr)
return false;
const std::string& segment_value = *segments[col_idx_];
return segment_value >= value_ && segment_value <= value2_;
}
Expand All @@ -154,7 +181,8 @@ TagNotBetween::TagNotBetween(int col_idx, std::string lower_value,
}

bool TagNotBetween::satisfyRow(std::vector<std::string*> segments) const {
if (col_idx_ >= segments.size()) return false;
if (col_idx_ >= segments.size() || segments[col_idx_] == nullptr)
return false;
const std::string& segment_value = *segments[col_idx_];
return segment_value < value_ || segment_value > value2_;
}
Expand Down Expand Up @@ -254,6 +282,18 @@ Filter* TagFilterBuilder::not_reg_exp(const std::string& columnName,
return new TagNotRegExp(idx, value);
}

Filter* TagFilterBuilder::is_null(const std::string& columnName) {
auto idx = get_id_column_index(columnName);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

id -> tag

if (idx < 0) return nullptr;
return new TagIsNull(idx);
}

Filter* TagFilterBuilder::is_not_null(const std::string& columnName) {
auto idx = get_id_column_index(columnName);
if (idx < 0) return nullptr;
return new TagIsNotNull(idx);
}

Filter* TagFilterBuilder::between_and(const std::string& columnName,
const std::string& lower,
const std::string& upper) {
Expand Down
17 changes: 17 additions & 0 deletions cpp/src/reader/filter/tag_filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,21 @@ class TagNotRegExp : public TagFilter {
bool satisfyRow(std::vector<std::string*> segments) const override;
};

// IS NULL: tag column has no value for this device. An absent trailing
// segment (col_idx_ beyond the device's segment count) is also treated as null.
class TagIsNull : public TagFilter {
public:
explicit TagIsNull(int col_idx);
bool satisfyRow(std::vector<std::string*> segments) const override;
};

// IS NOT NULL: tag column has a concrete value for this device.
class TagIsNotNull : public TagFilter {
public:
explicit TagIsNotNull(int col_idx);
bool satisfyRow(std::vector<std::string*> segments) const override;
};

// Range query [value_, value2_]
class TagBetween : public TagFilter {
public:
Expand Down Expand Up @@ -171,6 +186,8 @@ class TagFilterBuilder {
Filter* reg_exp(const std::string& columnName, const std::string& value);
Filter* not_reg_exp(const std::string& columnName,
const std::string& value);
Filter* is_null(const std::string& columnName);
Filter* is_not_null(const std::string& columnName);
Filter* between_and(const std::string& columnName, const std::string& lower,
const std::string& upper);
Filter* not_between_and(const std::string& columnName,
Expand Down
83 changes: 83 additions & 0 deletions cpp/test/reader/filter/tag_filter_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -414,4 +414,87 @@ TEST_F(TagFilterTest, TagRegExpEdgeCases) {

delete invalid_filter;
delete empty_filter;
}

// A null tag segment must not crash comparison filters; a null value never
// satisfies a concrete-value predicate (SQL UNKNOWN -> not matched).
TEST_F(TagFilterTest, ComparisonFiltersTreatNullSegmentAsNoMatch) {
// "name" (col 1) is an explicit null pointer.
std::vector<std::string*> segments = {nullptr, nullptr,
new std::string("25"),
new std::string("engineering")};

auto eq = builder_->eq("name", "john");
auto neq = builder_->neq("name", "john");
auto lt = builder_->lt("name", "z");
auto gt = builder_->gt("name", "a");
auto between = builder_->between_and("name", "a", "z");
auto reg = builder_->reg_exp("name", ".*");

EXPECT_FALSE(eq->satisfyRow(0, segments));
EXPECT_FALSE(neq->satisfyRow(0, segments));
EXPECT_FALSE(lt->satisfyRow(0, segments));
EXPECT_FALSE(gt->satisfyRow(0, segments));
EXPECT_FALSE(between->satisfyRow(0, segments));
EXPECT_FALSE(reg->satisfyRow(0, segments));

delete eq;
delete neq;
delete lt;
delete gt;
delete between;
delete reg;
delete segments[2];
delete segments[3];
}

// IS NULL filter
TEST_F(TagFilterTest, TagIsNullFilter) {
auto filter = builder_->is_null("name");
ASSERT_NE(filter, nullptr);

// "name" is an explicit null pointer.
std::vector<std::string*> null_seg = {nullptr, nullptr,
new std::string("25"),
new std::string("engineering")};
EXPECT_TRUE(filter->satisfyRow(0, null_seg));
delete null_seg[2];
delete null_seg[3];

// "name" has a concrete value.
auto present = createSegments("john", "25", "engineering");
EXPECT_FALSE(filter->satisfyRow(0, present));
cleanupSegments(present);

// A trailing tag column omitted from the device id (segment count too
// small) is also treated as null.
auto trailing = builder_->is_null("score"); // col_idx 5
std::vector<std::string*> short_seg = {nullptr, new std::string("john")};
EXPECT_TRUE(trailing->satisfyRow(0, short_seg));
delete short_seg[1];

delete filter;
delete trailing;
}

// IS NOT NULL filter
TEST_F(TagFilterTest, TagIsNotNullFilter) {
auto filter = builder_->is_not_null("name");
ASSERT_NE(filter, nullptr);

auto present = createSegments("john", "25", "engineering");
EXPECT_TRUE(filter->satisfyRow(0, present));
cleanupSegments(present);

std::vector<std::string*> null_seg = {nullptr, nullptr};
EXPECT_FALSE(filter->satisfyRow(0, null_seg));

// An omitted trailing tag is null, so IS NOT NULL is false.
auto trailing = builder_->is_not_null("score");
std::vector<std::string*> short_seg = {nullptr, new std::string("john")};
EXPECT_FALSE(trailing->satisfyRow(0, short_seg));
delete short_seg[1];

delete filter;
delete trailing;
}
16 changes: 8 additions & 8 deletions python/tests/test_reader_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def test_get_all_devices_segments():
assert d0.table_name == "root.sg"
assert d0.segments == ("root.sg", "py_details")

grp = reader.get_timeseries_metadata(None)[device]
grp = reader.get_timeseries_metadata(None)[d0.segments]
assert grp.table_name == "root.sg"
assert grp.segments == ("root.sg", "py_details")
assert len(grp.timeseries) == 1
Expand Down Expand Up @@ -103,8 +103,8 @@ def test_get_all_devices_and_timeseries_metadata_statistic():
assert devices[0].path == device

meta_all = reader.get_timeseries_metadata(None)
assert list(meta_all.keys()) == [device]
grp = meta_all[device]
assert list(meta_all.keys()) == [devices[0].segments]
grp = meta_all[devices[0].segments]
assert grp.table_name == "root.sg"
assert grp.segments == ("root.sg", "py_meta")
series = grp.timeseries
Expand All @@ -127,11 +127,11 @@ def test_get_all_devices_and_timeseries_metadata_statistic():
assert reader.get_timeseries_metadata([]) == {}

sub = reader.get_timeseries_metadata([DeviceID(device, None, ())])
assert device in sub
assert len(sub[device].timeseries) == 1
assert devices[0].segments in sub
assert len(sub[devices[0].segments].timeseries) == 1

sub_str = reader.get_timeseries_metadata([device])
assert device in sub_str
assert devices[0].segments in sub_str
finally:
reader.close()
try:
Expand Down Expand Up @@ -163,7 +163,7 @@ def test_get_timeseries_metadata_boolean_statistic():
reader = TsFileReader(path)
try:
meta_all = reader.get_timeseries_metadata(None)
st = meta_all[device].timeseries[0].statistic
st = meta_all[("root.sg", "py_bool")].timeseries[0].statistic
assert isinstance(st, BoolTimeseriesStatistic)
assert st.has_statistic
assert st.sum == pytest.approx(2.0)
Expand Down Expand Up @@ -200,7 +200,7 @@ def test_get_timeseries_metadata_string_statistic():
reader = TsFileReader(path)
try:
meta_all = reader.get_timeseries_metadata(None)
m = meta_all[device].timeseries[0]
m = meta_all[("root.sg", "py_str")].timeseries[0]
assert m.measurement_name == "m_str"
assert m.data_type == TSDataType.STRING
st = m.statistic
Expand Down
Loading
Loading