Skip to content

Commit cdbb998

Browse files
committed
feat: support LeafFunction of StartsWith, EndsWith, Contains, Like
1 parent 5443224 commit cdbb998

12 files changed

Lines changed: 273 additions & 101 deletions
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2026-present Alibaba Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "paimon/common/predicate/contains.h"
18+
19+
namespace paimon {
20+
21+
Result<bool> Contains::TestString(const std::string& field, const std::string& pattern) const {
22+
return field.find(pattern) != std::string::npos;
23+
}
24+
} // namespace paimon

src/paimon/common/predicate/contains.h

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ class Contains : public StringLeafBinaryFunction {
3030
return instance;
3131
}
3232

33-
Result<bool> TestString(const std::string& field, const std::string& pattern) const override {
34-
return field.find(pattern) != std::string::npos;
35-
}
36-
3733
Type GetType() const override {
3834
return Type::CONTAINS;
3935
}
@@ -42,6 +38,8 @@ class Contains : public StringLeafBinaryFunction {
4238
return "Contains";
4339
}
4440

41+
Result<bool> TestString(const std::string& field, const std::string& pattern) const override;
42+
4543
private:
4644
Contains() = default;
4745
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2026-present Alibaba Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "paimon/common/predicate/ends_with.h"
18+
19+
#include "paimon/common/utils/string_utils.h"
20+
21+
namespace paimon {
22+
23+
Result<bool> EndsWith::TestString(const std::string& field, const std::string& pattern) const {
24+
return StringUtils::EndsWith(field, pattern);
25+
}
26+
} // namespace paimon

src/paimon/common/predicate/ends_with.h

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,6 @@ class EndsWith : public StringLeafBinaryFunction {
3030
return instance;
3131
}
3232

33-
Result<bool> TestString(const std::string& field, const std::string& pattern) const override {
34-
return field.size() >= pattern.size() &&
35-
std::equal(pattern.rbegin(), pattern.rend(), field.rbegin());
36-
}
37-
3833
Type GetType() const override {
3934
return Type::ENDS_WITH;
4035
}
@@ -43,6 +38,8 @@ class EndsWith : public StringLeafBinaryFunction {
4338
return "EndsWith";
4439
}
4540

41+
Result<bool> TestString(const std::string& field, const std::string& pattern) const override;
42+
4643
private:
4744
EndsWith() = default;
4845
};
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright 2026-present Alibaba Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "paimon/common/predicate/like.h"
18+
19+
namespace paimon {
20+
21+
Result<bool> Like::TestString(const std::string& field, const std::string& pattern) const {
22+
if (pattern.empty()) {
23+
return Result<bool>(field.empty());
24+
}
25+
std::vector<char> pat;
26+
std::vector<bool> is_wild;
27+
for (size_t i = 0; i < pattern.size(); ++i) {
28+
if (pattern[i] == '\\' && i + 1 < pattern.size()) {
29+
pat.push_back(pattern[i + 1]);
30+
is_wild.push_back(false);
31+
++i;
32+
} else {
33+
char c = pattern[i];
34+
pat.push_back(c);
35+
is_wild.push_back(c == '_' || c == '%');
36+
}
37+
}
38+
std::vector<char> simp_pat;
39+
std::vector<bool> simp_wild;
40+
for (size_t i = 0; i < pat.size(); ++i) {
41+
if (is_wild[i] && pat[i] == '%' && !simp_pat.empty() && simp_wild.back() &&
42+
simp_pat.back() == '%') {
43+
continue;
44+
}
45+
simp_pat.push_back(pat[i]);
46+
simp_wild.push_back(is_wild[i]);
47+
}
48+
const size_t m = field.size();
49+
const size_t n = simp_pat.size();
50+
if (field.empty()) {
51+
return Result<bool>(n == 1 && simp_wild[0] && simp_pat[0] == '%');
52+
}
53+
size_t min_len = 0;
54+
for (size_t i = 0; i < n; ++i) {
55+
if (!simp_wild[i]) {
56+
min_len++;
57+
}
58+
}
59+
if (min_len > m) {
60+
return Result<bool>(false);
61+
}
62+
constexpr size_t STACK_LIMIT = 128;
63+
std::unique_ptr<bool[]> dp_storage;
64+
bool* dp;
65+
if (n <= STACK_LIMIT) {
66+
dp = static_cast<bool*>(alloca((n + 1) * sizeof(bool)));
67+
} else {
68+
dp_storage = std::make_unique<bool[]>(n + 1);
69+
dp = dp_storage.get();
70+
}
71+
std::fill_n(dp, n + 1, false);
72+
dp[0] = true;
73+
for (size_t j = 1; j <= n && simp_wild[j - 1] && simp_pat[j - 1] == '%'; ++j) {
74+
dp[j] = true;
75+
}
76+
const char* f = field.data();
77+
for (size_t i = 0; i < m; ++i) {
78+
const char sc = f[i];
79+
bool prev = dp[0];
80+
dp[0] = false;
81+
bool has_match = false;
82+
for (size_t j = 1; j <= n; ++j) {
83+
const bool temp = dp[j];
84+
const char pc = simp_pat[j - 1];
85+
const bool wild = simp_wild[j - 1];
86+
if (wild && pc == '%') {
87+
dp[j] = dp[j - 1] || dp[j];
88+
} else if (wild && pc == '_') {
89+
dp[j] = prev;
90+
} else {
91+
dp[j] = (pc == sc) ? prev : false;
92+
}
93+
has_match |= dp[j];
94+
prev = temp;
95+
}
96+
if (!has_match) {
97+
return Result<bool>(false);
98+
}
99+
}
100+
return Result<bool>(dp[n]);
101+
}
102+
} // namespace paimon

src/paimon/common/predicate/like.h

Lines changed: 2 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -30,82 +30,6 @@ class Like : public StringLeafBinaryFunction {
3030
return instance;
3131
}
3232

33-
Result<bool> TestString(const std::string& field, const std::string& pattern) const override {
34-
if (pattern.empty()) {
35-
return Result<bool>(field.empty());
36-
}
37-
std::vector<char> pat;
38-
std::vector<bool> is_wild;
39-
for (size_t i = 0; i < pattern.size(); ++i) {
40-
if (pattern[i] == '\\' && i + 1 < pattern.size()) {
41-
pat.push_back(pattern[i + 1]);
42-
is_wild.push_back(false);
43-
++i;
44-
} else {
45-
char c = pattern[i];
46-
pat.push_back(c);
47-
is_wild.push_back(c == '_' || c == '%');
48-
}
49-
}
50-
std::vector<char> simp_pat;
51-
std::vector<bool> simp_wild;
52-
for (size_t i = 0; i < pat.size(); ++i) {
53-
if (is_wild[i] && pat[i] == '%' && !simp_pat.empty() && simp_wild.back() &&
54-
simp_pat.back() == '%') {
55-
continue;
56-
}
57-
simp_pat.push_back(pat[i]);
58-
simp_wild.push_back(is_wild[i]);
59-
}
60-
const size_t m = field.size();
61-
const size_t n = simp_pat.size();
62-
if (field.empty()) {
63-
return Result<bool>(n == 1 && simp_wild[0] && simp_pat[0] == '%');
64-
}
65-
size_t min_len = 0;
66-
for (size_t i = 0; i < n; ++i) {
67-
if (!simp_wild[i]) min_len++;
68-
}
69-
if (min_len > m) return Result<bool>(false);
70-
constexpr size_t STACK_LIMIT = 128;
71-
std::unique_ptr<bool[]> dp_storage;
72-
bool* dp;
73-
if (n <= STACK_LIMIT) {
74-
dp = static_cast<bool*>(alloca((n + 1) * sizeof(bool)));
75-
} else {
76-
dp_storage = std::make_unique<bool[]>(n + 1);
77-
dp = dp_storage.get();
78-
}
79-
std::fill_n(dp, n + 1, false);
80-
dp[0] = true;
81-
for (size_t j = 1; j <= n && simp_wild[j - 1] && simp_pat[j - 1] == '%'; ++j) {
82-
dp[j] = true;
83-
}
84-
const char* f = field.data();
85-
for (size_t i = 0; i < m; ++i) {
86-
const char sc = f[i];
87-
bool prev = dp[0];
88-
dp[0] = false;
89-
bool has_match = false;
90-
for (size_t j = 1; j <= n; ++j) {
91-
const bool temp = dp[j];
92-
const char pc = simp_pat[j - 1];
93-
const bool wild = simp_wild[j - 1];
94-
if (wild && pc == '%') {
95-
dp[j] = dp[j - 1] || dp[j];
96-
} else if (wild && pc == '_') {
97-
dp[j] = prev;
98-
} else {
99-
dp[j] = (pc == sc) ? prev : false;
100-
}
101-
has_match |= dp[j];
102-
prev = temp;
103-
}
104-
if (!has_match) return Result<bool>(false);
105-
}
106-
return Result<bool>(dp[n]);
107-
}
108-
10933
Type GetType() const override {
11034
return Type::LIKE;
11135
}
@@ -114,6 +38,8 @@ class Like : public StringLeafBinaryFunction {
11438
return "Like";
11539
}
11640

41+
Result<bool> TestString(const std::string& field, const std::string& pattern) const override;
42+
11743
private:
11844
Like() = default;
11945
};

src/paimon/common/predicate/predicate_builder.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,18 @@ Result<std::shared_ptr<Predicate>> PredicateBuilder::Not(
162162
if (!predicate) {
163163
return Status::Invalid("There must not be nullptr to construct a NOT predicate");
164164
}
165-
return predicate->Negate();
165+
auto negate_predicate = predicate->Negate();
166+
if (!negate_predicate) {
167+
return Status::Invalid("Could not construct A NOT predicate from " + predicate->ToString());
168+
}
169+
return negate_predicate;
166170
}
167171

168172
std::shared_ptr<Predicate> PredicateBuilder::StartsWith(int32_t field_index,
169173
const std::string& field_name,
170174
const FieldType& field_type,
171175
const Literal& literal) {
176+
assert(field_type != FieldType::STRING || literal.GetType() != FieldType::STRING);
172177
return std::make_shared<LeafPredicateImpl>(StartsWith::Instance(), field_index, field_name,
173178
field_type, std::vector<Literal>({literal}));
174179
}
@@ -177,6 +182,7 @@ std::shared_ptr<Predicate> PredicateBuilder::EndsWith(int32_t field_index,
177182
const std::string& field_name,
178183
const FieldType& field_type,
179184
const Literal& literal) {
185+
assert(field_type != FieldType::STRING || literal.GetType() != FieldType::STRING);
180186
return std::make_shared<LeafPredicateImpl>(EndsWith::Instance(), field_index, field_name,
181187
field_type, std::vector<Literal>({literal}));
182188
}
@@ -185,6 +191,7 @@ std::shared_ptr<Predicate> PredicateBuilder::Contains(int32_t field_index,
185191
const std::string& field_name,
186192
const FieldType& field_type,
187193
const Literal& literal) {
194+
assert(field_type != FieldType::STRING || literal.GetType() != FieldType::STRING);
188195
return std::make_shared<LeafPredicateImpl>(Contains::Instance(), field_index, field_name,
189196
field_type, std::vector<Literal>({literal}));
190197
}
@@ -193,6 +200,7 @@ std::shared_ptr<Predicate> PredicateBuilder::Like(int32_t field_index,
193200
const std::string& field_name,
194201
const FieldType& field_type,
195202
const Literal& literal) {
203+
assert(field_type != FieldType::STRING || literal.GetType() != FieldType::STRING);
196204
return std::make_shared<LeafPredicateImpl>(Like::Instance(), field_index, field_name,
197205
field_type, std::vector<Literal>({literal}));
198206
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2026-present Alibaba Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "paimon/common/predicate/starts_with.h"
18+
19+
#include "paimon/common/utils/string_utils.h"
20+
21+
namespace paimon {
22+
23+
Result<bool> StartsWith::TestString(const std::string& field, const std::string& pattern) const {
24+
return StringUtils::StartsWith(field, pattern);
25+
}
26+
27+
Result<bool> StartsWith::Test(int64_t row_count, const Literal& min_value, const Literal& max_value,
28+
const std::optional<int64_t>& null_count,
29+
const Literal& pattern_literal) const {
30+
const auto min_str = min_value.GetValue<std::string>();
31+
const auto max_str = max_value.GetValue<std::string>();
32+
const auto pattern_str = pattern_literal.GetValue<std::string>();
33+
PAIMON_ASSIGN_OR_RAISE(const auto min_test, TestString(min_str, pattern_str));
34+
PAIMON_ASSIGN_OR_RAISE(const auto max_test, TestString(max_str, pattern_str));
35+
return (min_test || min_str.compare(pattern_str) <= 0) &&
36+
(max_test || max_str.compare(pattern_str) >= 0);
37+
}
38+
} // namespace paimon

0 commit comments

Comments
 (0)