blob: 20af55741bdcc09bf1d287f60eddf8c4411602ec [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 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 "exprs/like-predicate.h"
#include <string.h>
#include <string>
#include <vector>
#include "testutil/gtest-util.h"
namespace impala {
class LikePredicateTests {
public:
static void TestMatchOptimization();
static bool MatchConstant(const string& pattern, const string& value,
bool case_sensitive);
static bool MatchOptimized(const string& pattern, const string& value);
static bool MatchNonConstant(const string& pattern, const string& value);
private:
static LikePredicate::LikePredicateFunction GetMatchFunction(const string& pattern,
bool case_sensitive);
};
void LikePredicateTests::TestMatchOptimization() {
// Check that constant strings (with leading/trailing wildcard) use optimized
// check instead of regex match.
EXPECT_EQ(GetMatchFunction("test", true), &LikePredicate::ConstantEqualsFn);
EXPECT_EQ(GetMatchFunction("%test", true), &LikePredicate::ConstantEndsWithFn);
EXPECT_EQ(GetMatchFunction("test%", true), &LikePredicate::ConstantStartsWithFn);
EXPECT_EQ(GetMatchFunction("%test%", true), &LikePredicate::ConstantSubstringFn);
// Multiple % wilcards works the same as single ones.
EXPECT_EQ(GetMatchFunction("%%test", true), &LikePredicate::ConstantEndsWithFn);
EXPECT_EQ(GetMatchFunction("test%%", true), &LikePredicate::ConstantStartsWithFn);
EXPECT_EQ(GetMatchFunction("%%test%%", true), &LikePredicate::ConstantSubstringFn);
// Escaped wildcards are part of the constant string (IMPALA-10849, IMPALA-15641).
EXPECT_EQ(GetMatchFunction("\\%test", true), &LikePredicate::ConstantEqualsFn);
EXPECT_EQ(GetMatchFunction("test\\%", true), &LikePredicate::ConstantEqualsFn);
EXPECT_EQ(GetMatchFunction("\\%test\\%", true), &LikePredicate::ConstantEqualsFn);
EXPECT_EQ(GetMatchFunction("test\\%%", true), &LikePredicate::ConstantStartsWithFn);
EXPECT_EQ(GetMatchFunction("%\\%test", true), &LikePredicate::ConstantEndsWithFn);
EXPECT_EQ(GetMatchFunction("%te\\_st%", true), &LikePredicate::ConstantSubstringFn);
// An unescaped wildcard in the middle still needs a regex.
EXPECT_EQ(GetMatchFunction("test\\%_%", true), &LikePredicate::LikeFnPartial);
// A trailing escape character is a literal.
EXPECT_EQ(GetMatchFunction("test\\", true), &LikePredicate::ConstantEqualsFn);
EXPECT_EQ(GetMatchFunction("%test\\", true), &LikePredicate::ConstantEndsWithFn);
EXPECT_EQ(GetMatchFunction("test%\\", true), &LikePredicate::LikeFn);
// IMPALA-12374: Check that more complex patterns with trailing/leading % wildcard
// use partial regex match, instead of full regex match.
EXPECT_EQ(GetMatchFunction("%te%st", true), &LikePredicate::LikeFnPartial);
EXPECT_EQ(GetMatchFunction("te%st%", true), &LikePredicate::LikeFnPartial);
EXPECT_EQ(GetMatchFunction("%te%st%", true), &LikePredicate::LikeFnPartial);
EXPECT_EQ(GetMatchFunction("%te%st", false), &LikePredicate::LikeFnPartial);
EXPECT_EQ(GetMatchFunction("te%st%", false), &LikePredicate::LikeFnPartial);
EXPECT_EQ(GetMatchFunction("%te%st%", false), &LikePredicate::LikeFnPartial);
// Constant string match optimization is not enabled for case-insensitive,
// but partial match is.
EXPECT_EQ(GetMatchFunction("test", false), &LikePredicate::LikeFn);
EXPECT_EQ(GetMatchFunction("%test", false), &LikePredicate::LikeFnPartial);
EXPECT_EQ(GetMatchFunction("test%", false), &LikePredicate::LikeFnPartial);
EXPECT_EQ(GetMatchFunction("%test%", false), &LikePredicate::LikeFnPartial);
// Check case that cannot be optimized uses full regex match.
EXPECT_EQ(GetMatchFunction("te%st", true), &LikePredicate::LikeFn);
}
LikePredicate::LikePredicateFunction LikePredicateTests::GetMatchFunction(
const string& pattern, bool case_sensitive) {
LikePredicate::LikePredicateState test_state;
test_state.case_sensitive_ = case_sensitive;
test_state.function_ = &LikePredicate::LikeFn;
LikePredicate::OptimizeConstantPatternMatch(pattern, &test_state);
return test_state.function_;
}
// Runs a constant pattern through OptimizeConstantPatternMatch() and matches it
// against the given value the same way RegexMatch() does for constant patterns.
bool LikePredicateTests::MatchConstant(const string& pattern, const string& value,
bool case_sensitive) {
LikePredicate::LikePredicateState test_state;
test_state.case_sensitive_ = case_sensitive;
test_state.function_ = &LikePredicate::LikeFn;
LikePredicate::OptimizeConstantPatternMatch(pattern, &test_state);
re2::StringPiece operand(value.data(), value.size());
if (test_state.function_ == &LikePredicate::LikeFn) {
return RE2::FullMatch(operand, *test_state.regex_.get());
}
return RE2::PartialMatch(operand, *test_state.regex_.get());
}
// Matches 'value' case-sensitively the same way as the function that
// OptimizeConstantPatternMatch() chooses for the constant 'pattern'.
bool LikePredicateTests::MatchOptimized(const string& pattern, const string& value) {
LikePredicate::LikePredicateState test_state;
test_state.case_sensitive_ = true;
test_state.function_ = &LikePredicate::LikeFn;
LikePredicate::OptimizeConstantPatternMatch(pattern, &test_state);
const string& search = test_state.search_string_;
LikePredicate::LikePredicateFunction fn = test_state.function_;
if (fn == &LikePredicate::ConstantEqualsFn) return value == search;
if (fn == &LikePredicate::ConstantStartsWithFn) {
return value.compare(0, search.size(), search) == 0;
}
if (fn == &LikePredicate::ConstantEndsWithFn) {
return value.size() >= search.size() &&
value.compare(value.size() - search.size(), search.size(), search) == 0;
}
if (fn == &LikePredicate::ConstantSubstringFn) {
return value.find(search) != string::npos;
}
return MatchConstant(pattern, value, true);
}
// Matches 'value' case-sensitively the same way as RegexMatch() does for a pattern that
// is not constant.
bool LikePredicateTests::MatchNonConstant(const string& pattern, const string& value) {
LikePredicate::LikePredicateState test_state;
StringVal pattern_val(
reinterpret_cast<uint8_t*>(const_cast<char*>(pattern.data())), pattern.size());
string re_pattern;
LikePredicate::ConvertLikePattern(&test_state, pattern_val, &re_pattern);
RE2::Options opts;
opts.set_never_nl(false);
opts.set_dot_nl(true);
RE2 re(re_pattern, opts);
return RE2::FullMatch(re2::StringPiece(value.data(), value.size()), re);
}
TEST(TestLikePredicate, RegexMatchOptimization) {
LikePredicateTests::TestMatchOptimization();
}
// IMPALA-15547 regression: constant LIKE patterns with embedded NUL bytes must not
// be truncated at the first NUL when converting the LIKE pattern to a regex. The
// truncation was introduced by the IMPALA-12374 constant pattern optimization.
TEST(TestLikePredicate, RegexMatchWithEmbeddedNul) {
// Untrimmed pattern: the trailing % must not make the suffix after the NUL
// disappear. Before the fix the pattern was truncated to "a%" and "ax" matched.
EXPECT_FALSE(LikePredicateTests::MatchConstant(
std::string("a%\0b", 4), "ax", true));
EXPECT_TRUE(LikePredicateTests::MatchConstant(
std::string("a%\0b", 4), std::string("a\0b", 3), true));
// Pattern with leading and trailing %: the trimmed pattern must keep the bytes
// after the NUL. Before the fix it was truncated to "a", so any value containing
// "a" (e.g. "xa\0") matched.
EXPECT_TRUE(LikePredicateTests::MatchConstant(
std::string("%a\0_%", 5), std::string("xa\0c", 4), true));
EXPECT_TRUE(LikePredicateTests::MatchConstant(
std::string("%a\0_%", 5), std::string("xa\0cc", 5), true));
EXPECT_FALSE(LikePredicateTests::MatchConstant(
std::string("%a\0_%", 5), std::string("xa\0", 3), true));
// Case-insensitive (ILIKE) takes the same regex conversion path.
EXPECT_TRUE(LikePredicateTests::MatchConstant(
std::string("%a\0_%", 5), std::string("xA\0C", 4), false));
EXPECT_FALSE(LikePredicateTests::MatchConstant(
std::string("%a\0_%", 5), std::string("xa\0", 3), false));
}
// IMPALA-15641: The fast paths for constant patterns must match the same values as the
// regex that is used for patterns that are not constant.
TEST(TestLikePredicate, ConstantMatchesNonConstant) {
const std::vector<string> tokens = {"%", "_", "\\%", "\\_", "\\\\", "\\", "\\x", "x"};
std::vector<string> patterns;
for (const string& t : tokens) {
for (const string& p : {t + "ab", "a" + t + "b", "ab" + t, t + "ab%", "%ab" + t,
"%" + t + "%", "a" + t + "b%", "%a" + t + "b", "%a" + t + "b%"}) {
patterns.push_back(p);
}
}
const std::vector<string> values = {"", "ab", "xab", "axb", "abx", "%ab", "a%b",
"ab%", "_ab", "a_b", "ab_", "\\ab", "a\\b", "ab\\", "\\xab", "a\\xb", "ab\\x",
"a\\\\b", "x", "%", "_", "\\", "\\x", "ya\\bz", "ya%bz", "yaxbz"};
for (const string& p : patterns) {
for (const string& v : values) {
EXPECT_EQ(LikePredicateTests::MatchOptimized(p, v),
LikePredicateTests::MatchNonConstant(p, v))
<< "pattern: " << p << ", value: " << v;
}
}
// The escape character only escapes the wildcards and itself.
for (auto match : {&LikePredicateTests::MatchOptimized,
&LikePredicateTests::MatchNonConstant}) {
EXPECT_TRUE(match("C:\\dir%", "C:\\dir1"));
EXPECT_FALSE(match("C:\\dir%", "C:dir1"));
EXPECT_TRUE(match("a\\\\b%", "a\\bc"));
EXPECT_FALSE(match("a\\\\b%", "a\\\\bc"));
EXPECT_TRUE(match("a\\%%", "a%b"));
EXPECT_FALSE(match("a\\%%", "a\\b"));
EXPECT_TRUE(match("a%\\", "ab\\"));
EXPECT_FALSE(match("a%\\", "ab"));
}
}
} // namespace impala