blob: 2708ac95dd7d1839de3c716438511b5693fb9a14 [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 <re2/re2.h>
#include <re2/stringpiece.h>
#include <sstream>
#include "gutil/strings/substitute.h"
#include "runtime/string-value.inline.h"
#include "string-functions.h"
#include "util/utf8-util.h"
#include "common/names.h"
using namespace impala_udf;
using namespace re2;
namespace impala {
// A regex to match any regex pattern is equivalent to a substring search.
static const RE2 SUBSTRING_RE(
"(?:\\.\\*)*([^\\.\\^\\{\\[\\(\\|\\)\\]\\}\\+\\*\\?\\$\\\\]*)(?:\\.\\*)*");
// A regex to match any regex pattern which is equivalent to matching a constant string
// at the end of the string values.
static const RE2 ENDS_WITH_RE(
"(?:\\.\\*)*([^\\.\\^\\{\\[\\(\\|\\)\\]\\}\\+\\*\\?\\$\\\\]*)\\$");
// A regex to match any regex pattern which is equivalent to matching a constant string
// at the end of the string values.
static const RE2 STARTS_WITH_RE(
"\\^([^\\.\\^\\{\\[\\(\\|\\)\\]\\}\\+\\*\\?\\$\\\\]*)(?:\\.\\*)*");
// A regex to match any regex pattern which is equivalent to a constant string match.
static const RE2 EQUALS_RE("\\^([^\\.\\^\\{\\[\\(\\|\\)\\]\\}\\+\\*\\?\\$\\\\]*)\\$");
void LikePredicate::LikePrepare(FunctionContext* context,
FunctionContext::FunctionStateScope scope) {
LikePrepareInternal(context, scope, true);
}
void LikePredicate::ILikePrepare(FunctionContext* context,
FunctionContext::FunctionStateScope scope) {
LikePrepareInternal(context, scope, false);
}
// TODO: make class StringValue and StringSearch accept a case-sensitive flag and
// switch back to using the cheaper Constant<>() functions.
void LikePredicate::LikePrepareInternal(FunctionContext* context,
FunctionContext::FunctionStateScope scope, bool case_sensitive) {
if (scope != FunctionContext::THREAD_LOCAL) return;
LikePredicateState* state = new LikePredicateState();
context->SetFunctionState(scope, state);
state->function_ = LikeFn;
state->case_sensitive_ = case_sensitive;
if (context->IsArgConstant(1)) {
StringVal pattern_val = *reinterpret_cast<StringVal*>(context->GetConstantArg(1));
if (pattern_val.is_null) return;
StringValue pattern = StringValue::FromStringVal(pattern_val);
string pattern_str(pattern.Ptr(), pattern.Len());
OptimizeConstantPatternMatch(pattern_str, state);
if (state->regex_ && !state->regex_->ok()) {
context->SetError(Substitute("Invalid regex: $0", pattern_str).c_str());
}
}
}
void LikePredicate::LikeClose(FunctionContext* context,
FunctionContext::FunctionStateScope scope) {
if (scope == FunctionContext::THREAD_LOCAL) {
LikePredicateState* state = reinterpret_cast<LikePredicateState*>(
context->GetFunctionState(FunctionContext::THREAD_LOCAL));
delete state;
context->SetFunctionState(FunctionContext::THREAD_LOCAL, nullptr);
}
}
void LikePredicate::RegexPrepare(FunctionContext* context,
FunctionContext::FunctionStateScope scope) {
RegexPrepareInternal(context, scope, true);
}
void LikePredicate::IRegexPrepare(FunctionContext* context,
FunctionContext::FunctionStateScope scope) {
RegexPrepareInternal(context, scope, false);
}
void LikePredicate::RegexPrepareInternal(FunctionContext* context,
FunctionContext::FunctionStateScope scope, bool case_sensitive) {
if (scope != FunctionContext::THREAD_LOCAL) return;
LikePredicateState* state = new LikePredicateState();
context->SetFunctionState(scope, state);
state->function_ = RegexFn;
state->case_sensitive_ = case_sensitive;
if (context->IsArgConstant(1)) {
StringVal* pattern = reinterpret_cast<StringVal*>(context->GetConstantArg(1));
if (pattern->is_null) return;
string pattern_str(reinterpret_cast<const char*>(pattern->ptr), pattern->len);
string search_string;
// The following four conditionals check if the pattern is a constant string,
// starts with a constant string and is followed by any number of wildcard characters,
// ends with a constant string and is preceded by any number of wildcard characters or
// has a constant substring surrounded on both sides by any number of wildcard
// characters. In any of these conditions, we can search for the pattern more
// efficiently by using our own string match functions rather than regex matching.
if (case_sensitive && RE2::FullMatch(pattern_str, EQUALS_RE, &search_string)) {
state->SetSearchString(search_string);
state->function_ = ConstantEqualsFn;
} else if (case_sensitive &&
RE2::FullMatch(pattern_str, STARTS_WITH_RE, &search_string)) {
state->SetSearchString(search_string);
state->function_ = ConstantStartsWithFn;
} else if (case_sensitive &&
RE2::FullMatch(pattern_str, ENDS_WITH_RE, &search_string)) {
state->SetSearchString(search_string);
state->function_ = ConstantEndsWithFn;
} else if (case_sensitive &&
RE2::FullMatch(pattern_str, SUBSTRING_RE, &search_string)) {
state->SetSearchString(search_string);
state->function_ = ConstantSubstringFn;
} else {
RE2::Options opts;
opts.set_case_sensitive(case_sensitive);
StringFunctions::SetRE2MemOpt(&opts);
state->regex_.reset(new RE2(pattern_str, opts));
if (!state->regex_->ok()) {
context->SetError(
Substitute("Invalid regex expression: '$0'", pattern_str).c_str());
}
state->function_ = ConstantRegexFnPartial;
}
}
}
// This prepare function is used only when 3 parameters are passed to the regexp_like()
// function. For the 2 parameter version, the RegexPrepare() function is used to prepare.
void LikePredicate::RegexpLikePrepare(FunctionContext* context,
FunctionContext::FunctionStateScope scope) {
if (scope != FunctionContext::THREAD_LOCAL) return;
LikePredicateState* state = new LikePredicateState();
context->SetFunctionState(scope, state);
// If both the pattern and the match parameter are constant, we pre-compile the
// regular expression once here. Otherwise, the RE is compiled per row in RegexpLike()
if (context->IsArgConstant(1) && context->IsArgConstant(2)) {
StringVal* pattern;
pattern = reinterpret_cast<StringVal*>(context->GetConstantArg(1));
if (pattern->is_null) return;
StringVal* match_parameter = reinterpret_cast<StringVal*>(context->GetConstantArg(2));
stringstream error;
if (match_parameter->is_null) {
error << "NULL match parameter";
context->SetError(error.str().c_str());
return;
}
RE2::Options opts;
string error_str;
StringFunctions::SetRE2MemOpt(&opts);
if (!StringFunctions::SetRE2Options(*match_parameter, &error_str, &opts)) {
context->SetError(error_str.c_str());
return;
}
string pattern_str(reinterpret_cast<const char*>(pattern->ptr), pattern->len);
state->regex_.reset(new RE2(pattern_str, opts));
if (!state->regex_->ok()) {
context->SetError(
Substitute("Invalid regex expression: '$0'", pattern_str).c_str());
}
}
}
// This is used only for the 3 parameter version of regexp_like(). The 2 parameter
// version calls Regex() directly.
BooleanVal LikePredicate::RegexpLikeInternal(FunctionContext* context,
const StringVal& val, const StringVal& pattern, const StringVal& match_parameter) {
if (val.is_null || pattern.is_null) return BooleanVal::null();
// If either the pattern or the third optional match parameter are not constant, we
// have to recompile the RE for every row.
if (!context->IsArgConstant(2) || !context->IsArgConstant(1)) {
if (match_parameter.is_null) return BooleanVal::null();
RE2::Options opts;
string error_str;
StringFunctions::SetRE2MemOpt(&opts);
if (!StringFunctions::SetRE2Options(match_parameter, &error_str, &opts)) {
context->SetError(error_str.c_str());
return BooleanVal(false);
}
string re_pattern(reinterpret_cast<const char*>(pattern.ptr), pattern.len);
re2::RE2 re(re_pattern, opts);
if (re.ok()) {
return RE2::PartialMatch(
re2::StringPiece(reinterpret_cast<const char*>(val.ptr), val.len), re);
} else {
context->SetError(Substitute("Invalid regex: $0", re_pattern).c_str());
return BooleanVal(false);
}
}
return ConstantRegexFnPartial(context, val, pattern);
}
void LikePredicate::RegexClose(FunctionContext* context,
FunctionContext::FunctionStateScope scope) {
if (scope == FunctionContext::THREAD_LOCAL) {
LikePredicateState* state = reinterpret_cast<LikePredicateState*>(
context->GetFunctionState(FunctionContext::THREAD_LOCAL));
delete state;
context->SetFunctionState(FunctionContext::THREAD_LOCAL, nullptr);
}
}
BooleanVal LikePredicate::RegexFn(FunctionContext* context, const StringVal& val,
const StringVal& pattern) {
return RegexMatch(context, val, pattern, false);
}
BooleanVal LikePredicate::LikeFn(FunctionContext* context, const StringVal& val,
const StringVal& pattern) {
return RegexMatch(context, val, pattern, true);
}
BooleanVal LikePredicate::LikeFnPartial(FunctionContext* context, const StringVal& val,
const StringVal& pattern) {
// Handle partial LIKE as if it were a regexp_like, so that it uses PartialMatch
return RegexMatch(context, val, pattern, false);
}
BooleanVal LikePredicate::ConstantSubstringFn(FunctionContext* context,
const StringVal& val, const StringVal& pattern) {
if (val.is_null) return BooleanVal::null();
LikePredicateState* state = reinterpret_cast<LikePredicateState*>(
context->GetFunctionState(FunctionContext::THREAD_LOCAL));
if (state->search_string_sv_.Len() == 0) return BooleanVal(true);
StringValue pattern_value = StringValue::FromStringVal(val);
return BooleanVal(state->substring_pattern_.Search(&pattern_value) != -1);
}
BooleanVal LikePredicate::ConstantStartsWithFn(FunctionContext* context,
const StringVal& val, const StringVal& pattern) {
if (val.is_null) return BooleanVal::null();
LikePredicateState* state = reinterpret_cast<LikePredicateState*>(
context->GetFunctionState(FunctionContext::THREAD_LOCAL));
if (val.len < state->search_string_sv_.Len()) {
return BooleanVal(false);
} else {
StringValue v =
StringValue(reinterpret_cast<char*>(val.ptr), state->search_string_sv_.Len());
return BooleanVal(state->search_string_sv_.Eq((v)));
}
}
BooleanVal LikePredicate::ConstantEndsWithFn(FunctionContext* context,
const StringVal& val, const StringVal& pattern) {
if (val.is_null) return BooleanVal::null();
LikePredicateState* state = reinterpret_cast<LikePredicateState*>(
context->GetFunctionState(FunctionContext::THREAD_LOCAL));
int len = state->search_string_sv_.Len();
if (val.len < len) {
return BooleanVal(false);
} else {
char* ptr = reinterpret_cast<char*>(val.ptr) + val.len - len;
StringValue v = StringValue(ptr, len);
return BooleanVal(state->search_string_sv_.Eq(v));
}
}
BooleanVal LikePredicate::ConstantEqualsFn(FunctionContext* context, const StringVal& val,
const StringVal& pattern) {
if (val.is_null) return BooleanVal::null();
LikePredicateState* state = reinterpret_cast<LikePredicateState*>(
context->GetFunctionState(FunctionContext::THREAD_LOCAL));
return BooleanVal(state->search_string_sv_.Eq(StringValue::FromStringVal(val)));
}
BooleanVal LikePredicate::ConstantRegexFnPartial(FunctionContext* context,
const StringVal& val, const StringVal& pattern) {
if (val.is_null) return BooleanVal::null();
LikePredicateState* state = reinterpret_cast<LikePredicateState*>(
context->GetFunctionState(FunctionContext::THREAD_LOCAL));
re2::StringPiece operand_sp(reinterpret_cast<const char*>(val.ptr), val.len);
return RE2::PartialMatch(operand_sp, *state->regex_);
}
BooleanVal LikePredicate::ConstantRegexFn(FunctionContext* context,
const StringVal& val, const StringVal& pattern) {
if (val.is_null) return BooleanVal::null();
LikePredicateState* state = reinterpret_cast<LikePredicateState*>(
context->GetFunctionState(FunctionContext::THREAD_LOCAL));
re2::StringPiece operand_sp(reinterpret_cast<const char*>(val.ptr), val.len);
return RE2::FullMatch(operand_sp, *state->regex_);
}
BooleanVal LikePredicate::RegexMatch(FunctionContext* context,
const StringVal& operand_value, const StringVal& pattern_value,
bool is_like_pattern) {
if (operand_value.is_null || pattern_value.is_null) return BooleanVal::null();
LikePredicateState* state = reinterpret_cast<LikePredicateState*>(
context->GetFunctionState(FunctionContext::THREAD_LOCAL));
if (context->IsArgConstant(1)) {
if (is_like_pattern) {
return RE2::FullMatch(re2::StringPiece(reinterpret_cast<const char*>(
operand_value.ptr), operand_value.len), *state->regex_.get());
} else {
return RE2::PartialMatch(re2::StringPiece(reinterpret_cast<const char*>(
operand_value.ptr), operand_value.len), *state->regex_.get());
}
} else {
string re_pattern;
RE2::Options opts;
opts.set_case_sensitive(state->case_sensitive_);
StringFunctions::SetRE2MemOpt(&opts);
if (is_like_pattern) {
ConvertLikePattern(state, pattern_value, &re_pattern);
opts.set_never_nl(false);
opts.set_dot_nl(true);
} else {
re_pattern =
string(reinterpret_cast<const char*>(pattern_value.ptr), pattern_value.len);
}
re2::RE2 re(re_pattern, opts);
if (re.ok()) {
if (is_like_pattern) {
return RE2::FullMatch(re2::StringPiece(
reinterpret_cast<const char*>(operand_value.ptr), operand_value.len), re);
} else {
return RE2::PartialMatch(
re2::StringPiece(
reinterpret_cast<const char*>(operand_value.ptr), operand_value.len),
re);
}
} else {
string pattern_str(
reinterpret_cast<const char*>(pattern_value.ptr), pattern_value.len);
context->SetError(Substitute("Invalid regex: $0", pattern_str).c_str());
return BooleanVal(false);
}
}
}
// The escape character only escapes the wildcards and itself. Before any other
// character, or at the end of the pattern, it is a literal.
static bool IsEscape(char escape_char, const char* pattern, int len, int i) {
if (pattern[i] != escape_char || i + 1 == len) return false;
char next = pattern[i + 1];
return next == '%' || next == '_' || next == escape_char;
}
void LikePredicate::OptimizeConstantPatternMatch(const string &pattern,
LikePredicateState *state) {
// Split the pattern into its leading '%' wildcards, the part in the middle and its
// trailing '%' wildcards. Escapes are interpreted the same way as in
// ConvertLikePattern().
struct LikeToken {
char c;
bool is_wildcard;
// The position in 'pattern' after this character.
int end_pos;
};
vector<LikeToken> tokens;
bool is_escaped = false;
const int pattern_len = pattern.size();
for (int i = 0; i < pattern_len; ++i) {
char c = pattern[i];
if (!is_escaped && (c == '%' || c == '_')) {
tokens.push_back({c, true, i + 1});
} else if (!is_escaped &&
IsEscape(state->escape_char_, pattern.data(), pattern_len, i)) {
is_escaped = true;
} else {
tokens.push_back({c, false, i + 1});
is_escaped = false;
}
}
auto is_percent = [&tokens](int i) {
return tokens[i].is_wildcard && tokens[i].c == '%';
};
const int num_tokens = tokens.size();
int middle_begin = 0;
while (middle_begin < num_tokens && is_percent(middle_begin)) ++middle_begin;
int middle_end = num_tokens;
while (middle_end > middle_begin && is_percent(middle_end - 1)) --middle_end;
bool has_leading_wildcard = middle_begin > 0;
bool has_trailing_wildcard = middle_end < num_tokens;
// If the middle part has no wildcards, we can search for its unescaped form more
// efficiently by using our own string match functions rather than regex matching.
string search_string;
bool is_constant_middle = true;
for (int i = middle_begin; i < middle_end; ++i) {
if (tokens[i].is_wildcard) {
is_constant_middle = false;
break;
}
search_string.push_back(tokens[i].c);
}
// The regex path fails for patterns that are not valid UTF-8, also when they are not
// constant. The fast paths must not hide that.
if (state->case_sensitive_ && is_constant_middle &&
IsValidUtf8(pattern.data(), pattern.size())) {
state->SetSearchString(search_string);
if (has_leading_wildcard && has_trailing_wildcard) {
state->function_ = ConstantSubstringFn;
} else if (has_trailing_wildcard) {
state->function_ = ConstantStartsWithFn;
} else if (has_leading_wildcard) {
state->function_ = ConstantEndsWithFn;
} else {
state->function_ = ConstantEqualsFn;
}
return;
}
// IMPALA-12374:
// Avoid leading/trailing .* in regex, use (anchored) partial match instead.
// The leading '%' wildcards are the first characters of the pattern, because nothing
// escapes them. The middle part ends where the first trailing '%' wildcard is.
int raw_begin = middle_begin;
int raw_end = has_trailing_wildcard ? tokens[middle_end].end_pos - 1 : pattern_len;
StringVal re_pattern_val(
reinterpret_cast<uint8_t*>(const_cast<char*>(pattern.data() + raw_begin)),
raw_end - raw_begin);
bool re_full_match = !has_leading_wildcard && !has_trailing_wildcard;
// e.g. 'a%b%'
bool re_anchor_start = !has_leading_wildcard && has_trailing_wildcard;
// e.g. '%a%b'
bool re_anchor_end = has_leading_wildcard && !has_trailing_wildcard;
string re_pattern;
ConvertLikePattern(state, re_pattern_val, &re_pattern, re_anchor_start,
re_anchor_end);
RE2::Options opts;
opts.set_never_nl(false);
opts.set_dot_nl(true);
opts.set_case_sensitive(state->case_sensitive_);
StringFunctions::SetRE2MemOpt(&opts);
state->regex_.reset(new RE2(re_pattern, opts));
if (re_full_match) {
state->function_ = LikeFn;
} else {
state->function_ = LikeFnPartial;
}
}
void LikePredicate::ConvertLikePattern(LikePredicateState* state,
const StringVal& pattern, string* re_pattern, bool anchor_start,
bool anchor_end) {
re_pattern->clear();
if (anchor_start) {
re_pattern->append("^");
}
bool is_escaped = false;
for (int i = 0; i < pattern.len; ++i) {
if (!is_escaped && pattern.ptr[i] == '%') {
re_pattern->append(".*");
} else if (!is_escaped && pattern.ptr[i] == '_') {
re_pattern->append(".");
// check for escape char before checking for regex special chars, they might overlap
} else if (!is_escaped && IsEscape(state->escape_char_,
reinterpret_cast<const char*>(pattern.ptr), pattern.len, i)) {
is_escaped = true;
} else if (
pattern.ptr[i] == '.'
|| pattern.ptr[i] == '['
|| pattern.ptr[i] == ']'
|| pattern.ptr[i] == '{'
|| pattern.ptr[i] == '}'
|| pattern.ptr[i] == '('
|| pattern.ptr[i] == ')'
|| pattern.ptr[i] == '\\'
|| pattern.ptr[i] == '*'
|| pattern.ptr[i] == '+'
|| pattern.ptr[i] == '?'
|| pattern.ptr[i] == '|'
|| pattern.ptr[i] == '^'
|| pattern.ptr[i] == '$'
) {
// escape all regex special characters; see list at
// http://www.boost.org/doc/libs/1_47_0/libs/regex/doc/html/boost_regex/syntax/basic_extended.html
re_pattern->append("\\");
re_pattern->append(1, pattern.ptr[i]);
is_escaped = false;
} else {
// regular character or escaped special character
re_pattern->append(1, pattern.ptr[i]);
is_escaped = false;
}
}
if (anchor_end) {
re_pattern->append("$");
}
}
} // namespace impala