This is an automated email from the ASF dual-hosted git repository.
HappenLee pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 6b57f55e240 [fix](function) Reject invalid regex pattern read from a
column instead of returning NULL (#68359)
6b57f55e240 is described below
commit 6b57f55e2402e11f2dd74b3402a633030c809326
Author: Jerry Hu <[email protected]>
AuthorDate: Mon Oct 12 12:17:19 2026 +0800
[fix](function) Reject invalid regex pattern read from a column instead of
returning NULL (#68359)
### What problem does this PR solve?
Issue Number: None
Problem Summary:
`regexp_extract`, `regexp_extract_or_null`, `regexp_extract_all`,
`regexp_extract_all_array`, `regexp_replace` and `regexp_replace_one`
handled
an invalid regex pattern differently depending on where the pattern came
from.
A constant pattern is compiled in `open()` and the query fails with
`INVALID_ARGUMENT`. A pattern read from a column is compiled per row in
the
execute path, and the same compile failure was swallowed: the row was
turned
into NULL and only a warning was recorded.
```sql
CREATE TABLE t (id INT, s STRING, p STRING, repl STRING) ...;
INSERT INTO t VALUES (1, 'abc', '[', 'x');
SELECT regexp_extract('abc', '[', 0); -- INVALID_ARGUMENT
SELECT regexp_extract(s, p, 0) FROM t; -- NULL
SELECT regexp_replace('abc', '[', 'x'); -- INVALID_ARGUMENT
SELECT regexp_replace(s, p, repl) FROM t; -- NULL
```
So the same malformed pattern was either rejected or silently produced
NULL
depending on the physical shape of the argument, which hides data
problems
behind an unrelated NULL. `regexp_count` and `regexp` already raised the
error
on the column path.
This PR makes the column path of the extract/replace families throw the
same
`Status::InvalidArgument` that the constant path returns from `open()`,
so an
invalid pattern always fails the query. The null-map plumbing that only
existed
to mark those rows is removed, together with the now-unused `push_null`
handlers of `regexp_extract_all`.
Raising an error on the column path must not swallow SQL NULL
propagation. The
framework's default NULL handling strips a Nullable argument to its
nested
column and executes every row, and the bytes under a NULL slot are
whatever
the producer left there, so `regexp_replace(s, concat(p, if(id = 2,
NULL, '')), repl)`
could still carry `[` under a NULL pattern. The regexp functions
therefore
handle NULL themselves now: a shared helper strips Nullable from the
arguments,
ORs their null maps into the result null map and short-circuits a NULL
constant
argument, and the execute loops skip NULL rows. No bytes under a NULL
slot are
ever compiled and no argument column is copied.
The shared helper chooses the NULL handling once per block. When no
argument
is Nullable (`have_null_column()`), the original columns go straight to
the
function with no input NULL map and the per-row NULL checks compiled out
(`if constexpr (CheckNull)`); only Nullable arguments take the unnesting
and
NULL-row skipping path. Input NULL checking is kept separate from output
nullability: `regexp_extract_or_null` still returns NULL on no match for
non-nullable input, and a nullable `regexp_count` result still gets its
wrapper. `regexp_count`, the replace family and the extract family all
go
through this one dispatch.
### Release note
`regexp_extract`, `regexp_extract_or_null`, `regexp_extract_all`,
`regexp_extract_all_array`, `regexp_replace` and `regexp_replace_one`
now fail
the query with `INVALID_ARGUMENT` when a regex pattern read from a
column does
not compile, matching the behavior for constant patterns. Previously
such rows
returned NULL.
### Check List (For Author)
- Test:
- Unit Test: `FunctionLikeTest.regexp_invalid_column_pattern_fails`
covers
every family with a mixed valid/invalid pattern column;
`FunctionLikeTest.regexp_null_pattern_hides_invalid_payload` covers a
nullable pattern column whose NULL row (placed between valid rows)
hides an invalid payload, a NULL constant pattern, and a NULL input
string with all-constant parameters;
`FunctionLikeTest.regexp_count_non_nullable_arguments` and
`FunctionLikeTest.regexp_non_nullable_arguments` cover non-nullable
arguments with column and constant patterns, including the NULL that
`regexp_extract_or_null` returns on no match; the existing
`FunctionLikeTest.*` and `function_string_test.*` still pass.
- Regression test: new `test_regexp_invalid_pattern` compares the
constant
and column paths for all families, checks NULL propagation with an
invalid payload under a NULL pattern between valid rows, and runs every
family over a NOT NULL table; `test_string_function_regexp`,
`test_regexp_count`, `test_regexp_replace`,
`test_regexp_repeated_search_anchor`, `test_regexp_chinese`,
`test_split_by_regexp`, `nereids_function_p0` `R` and
`fold_constant_nullable` pass locally.
- Behavior changed: Yes. An invalid pattern read from a column now fails
the
query instead of yielding NULL for that row.
- Does this need documentation: No
https://claude.ai/code/session_01CvzmDA8snJbHHhErBB58sB
https://claude.ai/code/session_01PqyWdaYLRvhohSJqcn14zH
---
be/src/exprs/function/function_regexp.cpp | 371 +++++++++++------
be/test/exprs/function/function_like_test.cpp | 458 +++++++++++++++++++++
.../test_regexp_invalid_pattern.out | 98 +++++
.../test_regexp_invalid_pattern.groovy | 173 ++++++++
4 files changed, 984 insertions(+), 116 deletions(-)
diff --git a/be/src/exprs/function/function_regexp.cpp
b/be/src/exprs/function/function_regexp.cpp
index a791e975c66..e4293c15b52 100644
--- a/be/src/exprs/function/function_regexp.cpp
+++ b/be/src/exprs/function/function_regexp.cpp
@@ -21,7 +21,9 @@
#include <stddef.h>
#include <unicode/utf8.h>
+#include <algorithm>
#include <boost/regex.hpp>
+#include <initializer_list>
#include <memory>
#include <string>
#include <string_view>
@@ -29,6 +31,7 @@
#include <utility>
#include <vector>
+#include "common/check.h"
#include "common/status.h"
#include "core/block/block.h"
#include "core/block/column_numbers.h"
@@ -45,9 +48,11 @@
#include "core/data_type/data_type_nullable.h"
#include "core/data_type/data_type_number.h"
#include "core/data_type/data_type_string.h"
+#include "core/field.h"
#include "core/string_ref.h"
#include "core/types.h"
#include "exec/common/stringop_substring.h"
+#include "exec/common/util.hpp"
#include "exprs/aggregate/aggregate_function.h"
#include "exprs/function/function.h"
#include "exprs/function/simple_function_factory.h"
@@ -75,6 +80,102 @@ static bool advance_re2_search_position(const char* data,
size_t size,
return true;
}
+// The regexp functions below handle NULL rows themselves
(use_default_implementation_for_nulls()
+// returns false). The framework's default path runs a function over the
nested column of a
+// Nullable argument, and the bytes stored under a NULL slot are whatever the
producer left there;
+// compiling them as a pattern could fail and abort a query whose result for
that row is simply
+// NULL.
+//
+// Strips Nullable from every argument into `nested_block` (a ColumnConst
wrapper stays, so the
+// const/full handling of the functions applies unchanged) and ORs the
argument null maps into
+// `null_map`, which the functions skip while executing and hand back as the
result null map.
+// Returns false when an argument is a NULL constant: the whole result is NULL.
+bool unnest_regexp_arguments(const Block& block, const ColumnNumbers&
arguments,
+ Block& nested_block, ColumnNumbers&
nested_arguments,
+ NullMap& null_map) {
+ for (const auto argument : arguments) {
+ const auto& column = block.get_by_position(argument);
+ NullableColumnInfo info;
+ if (column.type->is_nullable()) {
+ info = column.get_nullable_column_info();
+ if (info.only_null) {
+ return false;
+ }
+ if (info.has_null) {
+ // A ColumnConst holding NULL is only_null, so this is a full
column.
+ DCHECK(!info.is_const);
+ VectorizedUtils::update_null_map(null_map,
+
column.get_nullable_null_map_column()->get_data());
+ }
+ }
+ nested_arguments.push_back(nested_block.columns());
+ nested_block.insert(column.unnest_nullable(info, false));
+ }
+ return true;
+}
+
+// Shared execute() of the regexp functions, choosing the NULL handling once
per block. The
+// result column is `execute(check_null, block, arguments, null_map)`:
+// - no argument is Nullable: `check_null` is std::false_type,
`block`/`arguments` are the
+// original ones and `null_map` is nullptr, so the functions have no input
NULL row to skip;
+// - otherwise a NULL constant argument makes the result a NULL constant, else
`check_null` is
+// std::true_type, `block`/`arguments` are the Nullable-stripped arguments
and `null_map` marks
+// the NULL rows to skip.
+// Use regexp_result_null_map() to get the NULL map of a Nullable result.
+template <typename Execute>
+Status execute_regexp_with_nulls(Block& block, const ColumnNumbers& arguments,
uint32_t result,
+ size_t input_rows_count, Execute&& execute) {
+ auto& result_column = block.get_by_position(result);
+ if (!have_null_column(block, arguments)) {
+ result_column.column = execute(std::false_type {}, block, arguments,
nullptr);
+ return Status::OK();
+ }
+ auto null_map = ColumnUInt8::create(input_rows_count, 0);
+ Block nested_block;
+ ColumnNumbers nested_arguments;
+ if (!unnest_regexp_arguments(block, arguments, nested_block,
nested_arguments,
+ null_map->get_data())) {
+ result_column.column = result_column.type->create_column_const(
+ input_rows_count, Field::create_field<TYPE_NULL>(Null()));
+ return Status::OK();
+ }
+ result_column.column =
+ execute(std::true_type {}, nested_block, nested_arguments,
std::move(null_map));
+ return Status::OK();
+}
+
+// The NULL map of a regexp function's Nullable result: starts as the input
NULL rows handed out
+// by execute_regexp_with_nulls(), or all zeros when there is none. Functions
may still mark
+// further rows NULL, e.g. regexp_extract_or_null on no match.
+template <bool CheckNull>
+ColumnUInt8::MutablePtr regexp_result_null_map(ColumnUInt8::MutablePtr
null_map,
+ size_t input_rows_count) {
+ if constexpr (CheckNull) {
+ DCHECK(null_map);
+ return null_map;
+ } else {
+ DCHECK(!null_map);
+ return ColumnUInt8::create(input_rows_count, 0);
+ }
+}
+
+// Collects the argument columns of a regexp function whose first argument is
the input string
+// and `parameters` are the pattern-side arguments: the input string is
materialized, and the
+// parameters stay unpacked constants when all of them are constant, which is
returned.
+template <size_t N>
+bool prepare_regexp_argument_columns(Block& block, const ColumnNumbers&
arguments,
+ const std::initializer_list<size_t>&
parameters,
+ ColumnPtr (&argument_columns)[N]) {
+ bool col_const[N];
+ for (size_t i = 0; i < N; ++i) {
+ col_const[i] =
is_column_const(*block.get_by_position(arguments[i]).column);
+ }
+ argument_columns[0] =
+
block.get_by_position(arguments[0]).column->convert_to_full_column_if_const();
+ default_preprocess_parameter_columns(argument_columns, col_const,
parameters, block, arguments);
+ return std::ranges::all_of(parameters, [&](size_t i) { return
col_const[i]; });
+}
+
// Helper structure to hold either RE2 or Boost.Regex
struct RegexpExtractEngine {
std::unique_ptr<re2::RE2> re2_regex;
@@ -211,11 +312,20 @@ struct RegexpExtractEngine {
struct RegexpCountImpl {
using StringColumnView = ColumnView<TYPE_STRING>;
+ // `null_map` holds the NULL rows to skip when CheckNull, and is nullptr
otherwise.
+ template <bool CheckNull>
static void execute_impl(FunctionContext* context, ColumnPtr
argument_columns[],
- size_t input_rows_count, ColumnInt32::Container&
result_data) {
+ size_t input_rows_count, ColumnInt32::Container&
result_data,
+ const NullMap* null_map) {
auto str_col = StringColumnView::create(argument_columns[0]);
auto pattern_col = StringColumnView::create(argument_columns[1]);
for (size_t i = 0; i < input_rows_count; ++i) {
+ if constexpr (CheckNull) {
+ if ((*null_map)[i]) {
+ result_data[i] = 0;
+ continue;
+ }
+ }
DCHECK(!str_col.is_null_at(i));
DCHECK(!pattern_col.is_null_at(i));
result_data[i] = _execute_inner_loop(context, str_col,
pattern_col, i);
@@ -223,7 +333,7 @@ struct RegexpCountImpl {
}
static int _execute_inner_loop(FunctionContext* context, const
StringColumnView& str_col,
const StringColumnView& pattern_col, const
size_t index_now) {
- re2::RE2* re = reinterpret_cast<re2::RE2*>(
+ auto* re = reinterpret_cast<re2::RE2*>(
context->get_function_state(FunctionContext::THREAD_LOCAL));
std::unique_ptr<re2::RE2> scoped_re;
if (re == nullptr) {
@@ -234,7 +344,6 @@ struct RegexpCountImpl {
if (!st) {
context->add_warning(error_str.c_str());
throw Exception(Status::InvalidArgument(error_str));
- return 0;
}
re = scoped_re.get();
}
@@ -278,6 +387,8 @@ public:
return std::make_shared<DataTypeInt32>();
}
+ bool use_default_implementation_for_nulls() const override { return false;
}
+
Status open(FunctionContext* context, FunctionContext::FunctionStateScope
scope) override {
if (scope == FunctionContext::THREAD_LOCAL) {
if (context->is_col_constant(1)) {
@@ -305,17 +416,32 @@ public:
Status execute_impl(FunctionContext* context, Block& block, const
ColumnNumbers& arguments,
uint32_t result, size_t input_rows_count) const
override {
- auto result_data_column = ColumnInt32::create(input_rows_count);
- auto& result_data = result_data_column->get_data();
-
- ColumnPtr argument_columns[2];
-
- argument_columns[0] = block.get_by_position(arguments[0]).column;
- argument_columns[1] = block.get_by_position(arguments[1]).column;
- RegexpCountImpl::execute_impl(context, argument_columns,
input_rows_count, result_data);
-
- block.get_by_position(result).column = std::move(result_data_column);
- return Status::OK();
+ const bool result_nullable =
block.get_by_position(result).type->is_nullable();
+ return execute_regexp_with_nulls(
+ block, arguments, result, input_rows_count,
+ [&](auto check_null, const Block& args_block, const
ColumnNumbers& args,
+ ColumnUInt8::MutablePtr null_map) -> ColumnPtr {
+ auto result_data_column =
ColumnInt32::create(input_rows_count);
+ ColumnPtr argument_columns[2] =
{args_block.get_by_position(args[0]).column,
+
args_block.get_by_position(args[1]).column};
+ const NullMap* input_null_map = nullptr;
+ if constexpr (check_null) {
+ input_null_map = &null_map->get_data();
+ }
+ RegexpCountImpl::execute_impl<check_null>(
+ context, argument_columns, input_rows_count,
+ result_data_column->get_data(), input_null_map);
+ if constexpr (check_null) {
+ // The result of regexp_count is nullable whenever an
argument is.
+ DORIS_CHECK(result_nullable);
+ }
+ if (!result_nullable) {
+ return result_data_column;
+ }
+ return
ColumnNullable::create(std::move(result_data_column),
+
regexp_result_null_map<check_null>(
+ std::move(null_map),
input_rows_count));
+ });
}
};
@@ -353,6 +479,8 @@ public:
return make_nullable(std::make_shared<DataTypeString>());
}
+ bool use_default_implementation_for_nulls() const override { return false;
}
+
DataTypes get_variadic_argument_types_impl() const override {
return ParamTypes::get_variadic_argument_types();
}
@@ -392,43 +520,40 @@ public:
Status execute_impl(FunctionContext* context, Block& block, const
ColumnNumbers& arguments,
uint32_t result, size_t input_rows_count) const
override {
- size_t argument_size = arguments.size();
-
- auto result_null_map = ColumnUInt8::create(input_rows_count, 0);
- auto result_data_column = ColumnString::create();
- auto& result_data = result_data_column->get_chars();
- auto& result_offset = result_data_column->get_offsets();
- result_offset.resize(input_rows_count);
-
- bool col_const[3];
- ColumnPtr argument_columns[3];
- for (int i = 0; i < 3; ++i) {
- col_const[i] =
is_column_const(*block.get_by_position(arguments[i]).column);
- }
- argument_columns[0] = col_const[0] ? static_cast<const ColumnConst&>(
-
*block.get_by_position(arguments[0]).column)
- .convert_to_full_column()
- :
block.get_by_position(arguments[0]).column;
-
- default_preprocess_parameter_columns(argument_columns, col_const, {1,
2}, block, arguments);
-
- StringRef options_value;
- if (col_const[1] && col_const[2]) {
- Impl::execute_impl_const_args(context, argument_columns,
options_value,
- input_rows_count, result_data,
result_offset,
- result_null_map->get_data());
- } else {
- // the options have check in FE, so is always const, and get idx
of 0
- if (argument_size == 4) {
- options_value =
block.get_by_position(arguments[3]).column->get_data_at(0);
- }
- Impl::execute_impl(context, argument_columns, options_value,
input_rows_count,
- result_data, result_offset,
result_null_map->get_data());
- }
+ return execute_regexp_with_nulls(
+ block, arguments, result, input_rows_count,
+ [&](auto check_null, Block& args_block, const ColumnNumbers&
args,
+ ColumnUInt8::MutablePtr null_map) -> ColumnPtr {
+ auto result_data_column = ColumnString::create();
+ auto& result_data = result_data_column->get_chars();
+ auto& result_offset = result_data_column->get_offsets();
+ result_offset.resize(input_rows_count);
+ auto result_null_map =
regexp_result_null_map<check_null>(std::move(null_map),
+
input_rows_count);
+
+ ColumnPtr argument_columns[3];
+ const bool parameters_const =
prepare_regexp_argument_columns(
+ args_block, args, {1, 2}, argument_columns);
+
+ StringRef options_value;
+ if (parameters_const) {
+ Impl::template execute_impl_const_args<check_null>(
+ context, argument_columns, options_value,
input_rows_count,
+ result_data, result_offset,
result_null_map->get_data());
+ } else {
+ // the options have check in FE, so is always const,
and get idx of 0
+ if (args.size() == 4) {
+ options_value =
+
args_block.get_by_position(args[3]).column->get_data_at(0);
+ }
+ Impl::template execute_impl<check_null>(
+ context, argument_columns, options_value,
input_rows_count,
+ result_data, result_offset,
result_null_map->get_data());
+ }
- block.get_by_position(result).column =
- ColumnNullable::create(std::move(result_data_column),
std::move(result_null_map));
- return Status::OK();
+ return
ColumnNullable::create(std::move(result_data_column),
+ std::move(result_null_map));
+ });
}
};
@@ -436,31 +561,47 @@ template <bool ReplaceOne>
struct RegexpReplaceImpl {
static constexpr auto name = ReplaceOne ? "regexp_replace_one" :
"regexp_replace";
+ // `null_map` holds the NULL rows to skip when CheckNull.
+ template <bool CheckNull>
static void execute_impl(FunctionContext* context, ColumnPtr
argument_columns[],
const StringRef& options_value, size_t
input_rows_count,
ColumnString::Chars& result_data,
ColumnString::Offsets& result_offset,
- NullMap& null_map) {
+ const NullMap& null_map) {
const auto* str_col =
check_and_get_column<ColumnString>(argument_columns[0].get());
const auto* pattern_col =
check_and_get_column<ColumnString>(argument_columns[1].get());
const auto* replace_col =
check_and_get_column<ColumnString>(argument_columns[2].get());
for (size_t i = 0; i < input_rows_count; ++i) {
+ if constexpr (CheckNull) {
+ if (null_map[i]) {
+ StringOP::push_empty_string(i, result_data, result_offset);
+ continue;
+ }
+ }
_execute_inner_loop<false>(context, str_col, pattern_col,
replace_col, options_value,
- result_data, result_offset, null_map,
i);
+ result_data, result_offset, i);
}
}
+ template <bool CheckNull>
static void execute_impl_const_args(FunctionContext* context, ColumnPtr
argument_columns[],
const StringRef& options_value, size_t
input_rows_count,
ColumnString::Chars& result_data,
- ColumnString::Offsets& result_offset,
NullMap& null_map) {
+ ColumnString::Offsets& result_offset,
+ const NullMap& null_map) {
const auto* str_col =
check_and_get_column<ColumnString>(argument_columns[0].get());
const auto* pattern_col =
check_and_get_column<ColumnString>(argument_columns[1].get());
const auto* replace_col =
check_and_get_column<ColumnString>(argument_columns[2].get());
for (size_t i = 0; i < input_rows_count; ++i) {
+ if constexpr (CheckNull) {
+ if (null_map[i]) {
+ StringOP::push_empty_string(i, result_data, result_offset);
+ continue;
+ }
+ }
_execute_inner_loop<true>(context, str_col, pattern_col,
replace_col, options_value,
- result_data, result_offset, null_map, i);
+ result_data, result_offset, i);
}
}
@@ -469,9 +610,8 @@ struct RegexpReplaceImpl {
const ColumnString* pattern_col,
const ColumnString* replace_col, const
StringRef& options_value,
ColumnString::Chars& result_data,
- ColumnString::Offsets& result_offset,
NullMap& null_map,
- const size_t index_now) {
- re2::RE2* re = reinterpret_cast<re2::RE2*>(
+ ColumnString::Offsets& result_offset,
const size_t index_now) {
+ auto* re = reinterpret_cast<re2::RE2*>(
context->get_function_state(FunctionContext::THREAD_LOCAL));
std::unique_ptr<re2::RE2> scoped_re; // destroys re if state->re is
nullptr
if (re == nullptr) {
@@ -481,8 +621,7 @@ struct RegexpReplaceImpl {
options_value, scoped_re);
if (!st) {
context->add_warning(error_str.c_str());
- StringOP::push_null_string(index_now, result_data,
result_offset, null_map);
- return;
+ throw Exception(Status::InvalidArgument(error_str));
}
re = scoped_re.get();
}
@@ -508,42 +647,36 @@ struct RegexpExtractImpl {
static DataTypePtr return_type() { return
make_nullable(std::make_shared<DataTypeString>()); }
- static Status execute(FunctionContext* context, Block& block, const
ColumnNumbers& arguments,
- uint32_t result, size_t input_rows_count) {
- bool col_const[3];
+ // See execute_regexp_with_nulls() for the arguments.
+ template <bool CheckNull>
+ static ColumnPtr execute(FunctionContext* context, Block& block, const
ColumnNumbers& arguments,
+ size_t input_rows_count, ColumnUInt8::MutablePtr
input_null_map) {
ColumnPtr argument_columns[3];
- for (int i = 0; i < 3; ++i) {
- col_const[i] =
is_column_const(*block.get_by_position(arguments[i]).column);
- }
- argument_columns[0] = col_const[0] ? static_cast<const ColumnConst&>(
-
*block.get_by_position(arguments[0]).column)
- .convert_to_full_column()
- :
block.get_by_position(arguments[0]).column;
+ const bool parameters_const =
+ prepare_regexp_argument_columns(block, arguments, {1, 2},
argument_columns);
- auto result_null_map = ColumnUInt8::create(input_rows_count, 0);
auto result_data_column = ColumnString::create();
auto& result_data = result_data_column->get_chars();
auto& result_offset = result_data_column->get_offsets();
result_offset.resize(input_rows_count);
+ auto result_null_map =
+ regexp_result_null_map<CheckNull>(std::move(input_null_map),
input_rows_count);
auto& null_map = result_null_map->get_data();
- default_preprocess_parameter_columns(argument_columns, col_const, {1,
2}, block, arguments);
-
- if (col_const[1] && col_const[2]) {
- _execute_loop<true>(context, argument_columns, input_rows_count,
result_data,
- result_offset, null_map);
+ if (parameters_const) {
+ _execute_loop<true, CheckNull>(context, argument_columns,
input_rows_count, result_data,
+ result_offset, null_map);
} else {
- _execute_loop<false>(context, argument_columns, input_rows_count,
result_data,
- result_offset, null_map);
+ _execute_loop<false, CheckNull>(context, argument_columns,
input_rows_count,
+ result_data, result_offset,
null_map);
}
- block.get_by_position(result).column =
- ColumnNullable::create(std::move(result_data_column),
std::move(result_null_map));
- return Status::OK();
+ return ColumnNullable::create(std::move(result_data_column),
std::move(result_null_map));
}
private:
- template <bool Const>
+ // `null_map` is the result NULL map; when CheckNull it starts with the
NULL rows to skip.
+ template <bool Const, bool CheckNull>
static void _execute_loop(FunctionContext* context, ColumnPtr
argument_columns[],
size_t input_rows_count, ColumnString::Chars&
result_data,
ColumnString::Offsets& result_offset, NullMap&
null_map) {
@@ -561,11 +694,23 @@ private:
return;
}
for (size_t i = 0; i < input_rows_count; ++i) {
+ if constexpr (CheckNull) {
+ if (null_map[i]) {
+ StringOP::push_empty_string(i, result_data,
result_offset);
+ continue;
+ }
+ }
_execute_inner_loop<true>(context, str_col, pattern_col,
index_data, result_data,
result_offset, null_map, i);
}
} else {
for (size_t i = 0; i < input_rows_count; ++i) {
+ if constexpr (CheckNull) {
+ if (null_map[i]) {
+ StringOP::push_empty_string(i, result_data,
result_offset);
+ continue;
+ }
+ }
const auto& index_data = index_col->get_int(i);
if (index_data < 0) {
ReturnNull ? StringOP::push_null_string(i, result_data,
result_offset, null_map)
@@ -596,8 +741,7 @@ private:
context->state()->enable_extended_regex());
if (!st) {
context->add_warning(error_str.c_str());
- StringOP::push_null_string(index_now, result_data,
result_offset, null_map);
- return;
+ throw Exception(Status::InvalidArgument(error_str));
}
engine = scoped_engine.get();
}
@@ -637,9 +781,6 @@ struct RegexpExtractAllStringOutput {
void push_empty(size_t index) {
StringOP::push_empty_string(index, result_data, result_offset);
}
- void push_null(size_t index, NullMap& null_map) {
- StringOP::push_null_string(index, result_data, result_offset,
null_map);
- }
void push_matches(size_t index, const std::vector<std::string>& matches) {
size_t total_size = 2; // '[' and ']'
for (const auto& m : matches) {
@@ -696,10 +837,6 @@ struct RegexpExtractAllArrayOutput {
UInt64 current_offset = 0;
void push_empty(size_t index) { array_offsets.push_back(current_offset); }
- void push_null(size_t index, NullMap& null_map) {
- null_map[index] = 1;
- array_offsets.push_back(current_offset);
- }
void push_matches(size_t index, const std::vector<std::string>& matches) {
for (const auto& m : matches) {
nested_col.insert_data(m.data(), m.size());
@@ -738,51 +875,46 @@ struct RegexpExtractAllImpl {
static DataTypePtr return_type() { return Handler::return_type(); }
- static Status execute(FunctionContext* context, Block& block, const
ColumnNumbers& arguments,
- uint32_t result, size_t input_rows_count) {
- bool col_const[2];
+ // See execute_regexp_with_nulls() for the arguments.
+ template <bool CheckNull>
+ static ColumnPtr execute(FunctionContext* context, Block& block, const
ColumnNumbers& arguments,
+ size_t input_rows_count, ColumnUInt8::MutablePtr
input_null_map) {
ColumnPtr argument_columns[2];
- for (int i = 0; i < 2; ++i) {
- col_const[i] =
is_column_const(*block.get_by_position(arguments[i]).column);
- }
- argument_columns[0] = col_const[0] ? static_cast<const ColumnConst&>(
-
*block.get_by_position(arguments[0]).column)
- .convert_to_full_column()
- :
block.get_by_position(arguments[0]).column;
-
- default_preprocess_parameter_columns(argument_columns, col_const, {1},
block, arguments);
+ const bool pattern_const =
+ prepare_regexp_argument_columns(block, arguments, {1},
argument_columns);
const auto* str_col =
check_and_get_column<ColumnString>(argument_columns[0].get());
const auto* pattern_col =
check_and_get_column<ColumnString>(argument_columns[1].get());
- auto outer_null_map = ColumnUInt8::create(input_rows_count, 0);
- auto& null_map_data = outer_null_map->get_data();
-
+ auto result_null_map =
+ regexp_result_null_map<CheckNull>(std::move(input_null_map),
input_rows_count);
+ const auto& null_map = result_null_map->get_data();
typename Handler::State state(input_rows_count);
auto handler = state.create_handler();
std::visit(
[&](auto is_const) {
for (size_t i = 0; i < input_rows_count; ++i) {
- if (null_map_data[i]) {
- handler.push_null(i, null_map_data);
- continue;
+ if constexpr (CheckNull) {
+ if (null_map[i]) {
+ handler.push_empty(i);
+ continue;
+ }
}
regexp_extract_all_inner_loop<is_const>(context,
str_col, pattern_col,
- handler,
null_map_data, i);
+ handler, i);
}
},
- make_bool_variant(col_const[1]));
+ make_bool_variant(pattern_const));
- block.get_by_position(result).column =
state.finalize(std::move(outer_null_map));
- return Status::OK();
+ return state.finalize(std::move(result_null_map));
}
private:
template <bool is_const>
static void regexp_extract_all_inner_loop(FunctionContext* context, const
ColumnString* str_col,
const ColumnString* pattern_col,
Handler& handler,
- NullMap& null_map, const size_t
index_now) {
+ const size_t index_now) {
auto* engine = reinterpret_cast<RegexpExtractEngine*>(
context->get_function_state(FunctionContext::THREAD_LOCAL));
std::unique_ptr<RegexpExtractEngine> scoped_engine;
@@ -795,8 +927,7 @@ private:
context->state()->enable_extended_regex());
if (!st) {
context->add_warning(error_str.c_str());
- handler.push_null(index_now, null_map);
- return;
+ throw Exception(Status::InvalidArgument(error_str));
}
engine = scoped_engine.get();
}
@@ -833,6 +964,8 @@ public:
return Impl::return_type();
}
+ bool use_default_implementation_for_nulls() const override { return false;
}
+
Status open(FunctionContext* context, FunctionContext::FunctionStateScope
scope) override {
if (scope == FunctionContext::THREAD_LOCAL) {
if (context->is_col_constant(Impl::PATTERN_ARG_IDX)) {
@@ -861,7 +994,13 @@ public:
Status execute_impl(FunctionContext* context, Block& block, const
ColumnNumbers& arguments,
uint32_t result, size_t input_rows_count) const
override {
- return Impl::execute(context, block, arguments, result,
input_rows_count);
+ return execute_regexp_with_nulls(
+ block, arguments, result, input_rows_count,
+ [&](auto check_null, Block& args_block, const ColumnNumbers&
args,
+ ColumnUInt8::MutablePtr null_map) {
+ return Impl::template execute<check_null>(
+ context, args_block, args, input_rows_count,
std::move(null_map));
+ });
}
};
diff --git a/be/test/exprs/function/function_like_test.cpp
b/be/test/exprs/function/function_like_test.cpp
index 645b517e716..ec88452bbbe 100644
--- a/be/test/exprs/function/function_like_test.cpp
+++ b/be/test/exprs/function/function_like_test.cpp
@@ -17,6 +17,7 @@
#include <cstdint>
#include <memory>
+#include <optional>
#include <string>
#include <vector>
@@ -839,6 +840,463 @@ TEST(FunctionLikeTest, regexp_replace_one) {
}
}
+// An invalid pattern read from a column must fail the whole call with the same
+// InvalidArgument error a constant pattern raises in open(), instead of
turning
+// the offending row into NULL.
+// NOLINTNEXTLINE(readability-function-size) -- one driver covers every regexp
function family.
+TEST(FunctionLikeTest, regexp_invalid_column_pattern_fails) {
+ auto str_type = std::make_shared<DataTypeString>();
+ auto int_type = std::make_shared<DataTypeInt64>();
+
+ auto make_str_col = [](const std::vector<std::string>& values) {
+ auto col = ColumnString::create();
+ for (const auto& value : values) {
+ col->insert_data(value.data(), value.size());
+ }
+ return col;
+ };
+ auto make_int_col = [](const std::vector<int64_t>& values) {
+ auto col = ColumnInt64::create();
+ for (auto value : values) {
+ col->insert_value(value);
+ }
+ return col;
+ };
+
+ // The first row compiles, only the second row carries an invalid pattern.
+ const std::vector<std::string> strs = {"abc", "abc"};
+ const std::vector<std::string> patterns = {"(b)", "["};
+
+ auto run_case = [&](const std::string& func_name, ColumnsWithTypeAndName
arg_cols,
+ const DataTypePtr& return_type, const std::string&
expected_error) {
+ Block block;
+ ColumnNumbers arguments;
+ std::vector<DataTypePtr> arg_types;
+ std::vector<std::shared_ptr<ColumnPtrWrapper>> constant_cols;
+ for (auto& arg : arg_cols) {
+ arguments.push_back(static_cast<unsigned int>(block.columns()));
+ arg_types.push_back(arg.type);
+ constant_cols.push_back(nullptr);
+ block.insert(std::move(arg));
+ }
+ auto func = SimpleFunctionFactory::instance().get_function(
+ func_name, block.get_columns_with_type_and_name(),
return_type);
+ ASSERT_TRUE(func != nullptr) << func_name;
+
+ auto result = block.columns();
+ block.insert({nullptr, return_type, "result"});
+
+ FunctionUtils fn_utils({}, arg_types, false);
+ auto* fn_ctx = fn_utils.get_fn_ctx();
+ fn_ctx->set_constant_cols(constant_cols);
+
+ ASSERT_EQ(Status::OK(), func->open(fn_ctx,
FunctionContext::FRAGMENT_LOCAL));
+ ASSERT_EQ(Status::OK(), func->open(fn_ctx,
FunctionContext::THREAD_LOCAL));
+
+ auto st = func->execute(fn_ctx, block, arguments, result, strs.size());
+ EXPECT_TRUE(st.is<ErrorCode::INVALID_ARGUMENT>()) << func_name << ": "
<< st;
+ EXPECT_NE(st.to_string().find(expected_error), std::string::npos)
+ << func_name << ": " << st;
+
+ static_cast<void>(func->close(fn_ctx, FunctionContext::THREAD_LOCAL));
+ static_cast<void>(func->close(fn_ctx,
FunctionContext::FRAGMENT_LOCAL));
+ };
+
+ auto nullable_str = make_nullable(str_type);
+ const std::string extract_error = "Invalid regex pattern";
+ const std::string re2_error = "Could not compile regexp pattern";
+
+ for (const char* func_name : {"regexp_extract", "regexp_extract_or_null"})
{
+ run_case(func_name,
+ {{make_str_col(strs), str_type, "str"},
+ {make_str_col(patterns), str_type, "pattern"},
+ {make_int_col({1, 1}), int_type, "idx"}},
+ nullable_str, extract_error);
+ }
+ run_case("regexp_extract_all",
+ {{make_str_col(strs), str_type, "str"}, {make_str_col(patterns),
str_type, "pattern"}},
+ nullable_str, extract_error);
+ run_case("regexp_extract_all_array",
+ {{make_str_col(strs), str_type, "str"}, {make_str_col(patterns),
str_type, "pattern"}},
+ make_nullable(std::make_shared<DataTypeArray>(nullable_str)),
extract_error);
+ for (const char* func_name : {"regexp_replace", "regexp_replace_one"}) {
+ run_case(func_name,
+ {{make_str_col(strs), str_type, "str"},
+ {make_str_col(patterns), str_type, "pattern"},
+ {make_str_col({"x", "x"}), str_type, "repl"}},
+ nullable_str, re2_error);
+ }
+ run_case("regexp_count",
+ {{make_str_col(strs), str_type, "str"}, {make_str_col(patterns),
str_type, "pattern"}},
+ std::make_shared<DataTypeInt32>(), re2_error);
+}
+
+// A SQL NULL pattern must yield NULL for that row even when the bytes stored
under the NULL
+// slot do not compile: the regexp functions skip NULL rows instead of running
the framework's
+// default NULL handling over the nested column. A NULL constant pattern
yields all NULLs, and a
+// NULL input string yields NULL for its row with constant parameters too.
+// NOLINTNEXTLINE(readability-function-size) -- one driver covers every regexp
function family.
+TEST(FunctionLikeTest, regexp_null_pattern_hides_invalid_payload) {
+ auto str_type = std::make_shared<DataTypeString>();
+ auto nullable_str = make_nullable(str_type);
+ auto int_type = std::make_shared<DataTypeInt64>();
+
+ auto make_str_col = [](const std::vector<std::string>& values) {
+ auto col = ColumnString::create();
+ for (const auto& value : values) {
+ col->insert_data(value.data(), value.size());
+ }
+ return col;
+ };
+ auto make_int_col = [](const std::vector<int64_t>& values) {
+ auto col = ColumnInt64::create();
+ for (auto value : values) {
+ col->insert_value(value);
+ }
+ return col;
+ };
+ auto make_null_map = [](const std::vector<uint8_t>& values) {
+ auto col = ColumnUInt8::create();
+ for (auto value : values) {
+ col->insert_value(value);
+ }
+ return col;
+ };
+ // The NULL row sits between two valid rows, so a skipped row must still
keep the result
+ // offsets of the rows after it right.
+ const std::vector<std::string> strs = {"abc", "abc", "abc"};
+ // Rows 0 and 2 are a valid pattern, row 1 is NULL with an invalid payload
underneath.
+ auto make_pattern_col = [&]() {
+ return ColumnNullable::create(make_str_col({"(b)", "[", "(b)"}),
make_null_map({0, 1, 0}));
+ };
+ // A NULL constant whose payload is an invalid pattern.
+ auto make_null_const_pattern = [&]() {
+ return ColumnConst::create(ColumnNullable::create(make_str_col({"["}),
make_null_map({1})),
+ strs.size());
+ };
+ // Row 1 is a NULL input string, for the case where every other parameter
is constant.
+ auto make_nullable_str_col = [&]() {
+ return ColumnNullable::create(make_str_col({"abc", "zzz", "abc"}),
+ make_null_map({0, 1, 0}));
+ };
+ auto make_const_str = [&](const std::string& value) {
+ return ColumnConst::create(make_str_col({value}), strs.size());
+ };
+ auto make_const_int = [&](int64_t value) {
+ return ColumnConst::create(make_int_col({value}), strs.size());
+ };
+
+ // Rows 0 and 2 are `expected`, std::nullopt meaning NULL; row 1 is always
NULL.
+ auto run_case = [&](const std::string& func_name, ColumnsWithTypeAndName
arg_cols,
+ const DataTypePtr& return_type,
+ const std::optional<std::string>& expected) {
+ Block block;
+ ColumnNumbers arguments;
+ std::vector<DataTypePtr> arg_types;
+ std::vector<std::shared_ptr<ColumnPtrWrapper>> constant_cols;
+ for (auto& arg : arg_cols) {
+ arguments.push_back(static_cast<unsigned int>(block.columns()));
+ arg_types.push_back(arg.type);
+ constant_cols.push_back(is_column_const(*arg.column)
+ ?
std::make_shared<ColumnPtrWrapper>(arg.column)
+ : nullptr);
+ block.insert(std::move(arg));
+ }
+ auto func = SimpleFunctionFactory::instance().get_function(
+ func_name, block.get_columns_with_type_and_name(),
return_type);
+ ASSERT_TRUE(func != nullptr) << func_name;
+
+ auto result = block.columns();
+ block.insert({nullptr, return_type, "result"});
+
+ FunctionUtils fn_utils({}, arg_types, false);
+ auto* fn_ctx = fn_utils.get_fn_ctx();
+ fn_ctx->set_constant_cols(constant_cols);
+
+ ASSERT_EQ(Status::OK(), func->open(fn_ctx,
FunctionContext::FRAGMENT_LOCAL)) << func_name;
+ ASSERT_EQ(Status::OK(), func->open(fn_ctx,
FunctionContext::THREAD_LOCAL)) << func_name;
+ ASSERT_EQ(Status::OK(), func->execute(fn_ctx, block, arguments,
result, strs.size()))
+ << func_name;
+
+ const auto& result_column = block.get_by_position(result).column;
+ ASSERT_EQ(strs.size(), result_column->size()) << func_name;
+ for (size_t row : {0, 2}) {
+ if (expected) {
+ EXPECT_EQ(*expected, return_type->to_string(*result_column,
row))
+ << func_name << " row " << row;
+ } else {
+ EXPECT_TRUE(result_column->is_null_at(row)) << func_name << "
row " << row;
+ }
+ }
+ EXPECT_TRUE(result_column->is_null_at(1)) << func_name;
+
+ static_cast<void>(func->close(fn_ctx, FunctionContext::THREAD_LOCAL));
+ static_cast<void>(func->close(fn_ctx,
FunctionContext::FRAGMENT_LOCAL));
+ };
+
+ for (const char* func_name : {"regexp_extract", "regexp_extract_or_null"})
{
+ run_case(func_name,
+ {{make_str_col(strs), str_type, "str"},
+ {make_pattern_col(), nullable_str, "pattern"},
+ {make_int_col({1, 1, 1}), int_type, "idx"}},
+ nullable_str, "b");
+ run_case(func_name,
+ {{make_str_col(strs), str_type, "str"},
+ {make_null_const_pattern(), nullable_str, "pattern"},
+ {make_int_col({1, 1, 1}), int_type, "idx"}},
+ nullable_str, std::nullopt);
+ run_case(func_name,
+ {{make_nullable_str_col(), nullable_str, "str"},
+ {make_const_str("(b)"), str_type, "pattern"},
+ {make_const_int(1), int_type, "idx"}},
+ nullable_str, "b");
+ }
+ run_case("regexp_extract_all",
+ {{make_str_col(strs), str_type, "str"}, {make_pattern_col(),
nullable_str, "pattern"}},
+ nullable_str, "['b']");
+ run_case("regexp_extract_all",
+ {{make_str_col(strs), str_type, "str"},
+ {make_null_const_pattern(), nullable_str, "pattern"}},
+ nullable_str, std::nullopt);
+ run_case("regexp_extract_all",
+ {{make_nullable_str_col(), nullable_str, "str"},
+ {make_const_str("(b)"), str_type, "pattern"}},
+ nullable_str, "['b']");
+ auto nullable_array =
make_nullable(std::make_shared<DataTypeArray>(nullable_str));
+ run_case("regexp_extract_all_array",
+ {{make_str_col(strs), str_type, "str"}, {make_pattern_col(),
nullable_str, "pattern"}},
+ nullable_array, "[\"b\"]");
+ run_case("regexp_extract_all_array",
+ {{make_str_col(strs), str_type, "str"},
+ {make_null_const_pattern(), nullable_str, "pattern"}},
+ nullable_array, std::nullopt);
+ run_case("regexp_extract_all_array",
+ {{make_nullable_str_col(), nullable_str, "str"},
+ {make_const_str("(b)"), str_type, "pattern"}},
+ nullable_array, "[\"b\"]");
+ for (const char* func_name : {"regexp_replace", "regexp_replace_one"}) {
+ run_case(func_name,
+ {{make_str_col(strs), str_type, "str"},
+ {make_pattern_col(), nullable_str, "pattern"},
+ {make_str_col({"x", "x", "x"}), str_type, "repl"}},
+ nullable_str, "axc");
+ run_case(func_name,
+ {{make_str_col(strs), str_type, "str"},
+ {make_null_const_pattern(), nullable_str, "pattern"},
+ {make_str_col({"x", "x", "x"}), str_type, "repl"}},
+ nullable_str, std::nullopt);
+ run_case(func_name,
+ {{make_nullable_str_col(), nullable_str, "str"},
+ {make_const_str("(b)"), str_type, "pattern"},
+ {make_const_str("x"), str_type, "repl"}},
+ nullable_str, "axc");
+ }
+ auto nullable_int32 = make_nullable(std::make_shared<DataTypeInt32>());
+ run_case("regexp_count",
+ {{make_str_col(strs), str_type, "str"}, {make_pattern_col(),
nullable_str, "pattern"}},
+ nullable_int32, "1");
+ run_case("regexp_count",
+ {{make_str_col(strs), str_type, "str"},
+ {make_null_const_pattern(), nullable_str, "pattern"}},
+ nullable_int32, std::nullopt);
+ run_case("regexp_count",
+ {{make_nullable_str_col(), nullable_str, "str"},
+ {make_const_str("(b)"), str_type, "pattern"}},
+ nullable_int32, "1");
+}
+
+// regexp_count over non-nullable arguments counts the original columns
directly and returns
+// the declared result type: a plain Int32 column, or a Nullable one without
NULL rows.
+TEST(FunctionLikeTest, regexp_count_non_nullable_arguments) {
+ auto str_type = std::make_shared<DataTypeString>();
+ auto int32_type = std::make_shared<DataTypeInt32>();
+
+ auto make_str_col = [](const std::vector<std::string>& values) {
+ auto col = ColumnString::create();
+ for (const auto& value : values) {
+ col->insert_data(value.data(), value.size());
+ }
+ return col;
+ };
+ const std::vector<std::string> strs = {"a1b22c333", "book keeper", "aaa"};
+
+ auto run_case = [&](ColumnsWithTypeAndName arg_cols, const DataTypePtr&
return_type,
+ const std::vector<int32_t>& expected) {
+ Block block;
+ ColumnNumbers arguments;
+ std::vector<DataTypePtr> arg_types;
+ std::vector<std::shared_ptr<ColumnPtrWrapper>> constant_cols;
+ for (auto& arg : arg_cols) {
+ arguments.push_back(static_cast<unsigned int>(block.columns()));
+ arg_types.push_back(arg.type);
+ constant_cols.push_back(is_column_const(*arg.column)
+ ?
std::make_shared<ColumnPtrWrapper>(arg.column)
+ : nullptr);
+ block.insert(std::move(arg));
+ }
+ auto func = SimpleFunctionFactory::instance().get_function(
+ "regexp_count", block.get_columns_with_type_and_name(),
return_type);
+ ASSERT_TRUE(func != nullptr);
+
+ auto result = block.columns();
+ block.insert({nullptr, return_type, "result"});
+
+ FunctionUtils fn_utils({}, arg_types, false);
+ auto* fn_ctx = fn_utils.get_fn_ctx();
+ fn_ctx->set_constant_cols(constant_cols);
+
+ ASSERT_EQ(Status::OK(), func->open(fn_ctx,
FunctionContext::FRAGMENT_LOCAL));
+ ASSERT_EQ(Status::OK(), func->open(fn_ctx,
FunctionContext::THREAD_LOCAL));
+ ASSERT_EQ(Status::OK(), func->execute(fn_ctx, block, arguments,
result, strs.size()));
+
+ const auto& result_column = block.get_by_position(result).column;
+ ASSERT_EQ(return_type->is_nullable(), result_column->is_nullable());
+ const IColumn* data_column = result_column.get();
+ if (return_type->is_nullable()) {
+ const auto& nullable = assert_cast<const
ColumnNullable&>(*result_column);
+ EXPECT_FALSE(nullable.has_null());
+ data_column = &nullable.get_nested_column();
+ }
+ const auto& data = assert_cast<const
ColumnInt32&>(*data_column).get_data();
+ ASSERT_EQ(expected.size(), data.size());
+ for (size_t i = 0; i < expected.size(); ++i) {
+ EXPECT_EQ(expected[i], data[i]) << "row " << i;
+ }
+
+ static_cast<void>(func->close(fn_ctx, FunctionContext::THREAD_LOCAL));
+ static_cast<void>(func->close(fn_ctx,
FunctionContext::FRAGMENT_LOCAL));
+ };
+
+ for (const auto& return_type : {DataTypePtr(int32_type),
make_nullable(int32_type)}) {
+ run_case({{make_str_col(strs), str_type, "str"},
+ {make_str_col({"\\d+", "oo|ee", "^a"}), str_type,
"pattern"}},
+ return_type, {3, 2, 1});
+ run_case({{make_str_col(strs), str_type, "str"},
+ {ColumnConst::create(make_str_col({"[a-z]"}), strs.size()),
str_type, "pattern"}},
+ return_type, {3, 10, 3});
+ }
+}
+
+// The extract and replace functions over non-nullable arguments run without
an input NULL map
+// but keep their Nullable result: no row is NULL except where the function
itself produces one,
+// e.g. regexp_extract_or_null on no match.
+// NOLINTNEXTLINE(readability-function-size) -- one driver covers every regexp
function family.
+TEST(FunctionLikeTest, regexp_non_nullable_arguments) {
+ auto str_type = std::make_shared<DataTypeString>();
+ auto nullable_str = make_nullable(str_type);
+ auto int_type = std::make_shared<DataTypeInt64>();
+
+ auto make_str_col = [](const std::vector<std::string>& values) {
+ auto col = ColumnString::create();
+ for (const auto& value : values) {
+ col->insert_data(value.data(), value.size());
+ }
+ return col;
+ };
+ auto make_int_col = [](const std::vector<int64_t>& values) {
+ auto col = ColumnInt64::create();
+ for (auto value : values) {
+ col->insert_value(value);
+ }
+ return col;
+ };
+ // Row 0 matches, row 1 does not, for both the column and the constant
pattern.
+ const std::vector<std::string> strs = {"abc", "xyz"};
+ auto make_column_pattern = [&]() { return make_str_col({"(b)", "(q)"}); };
+ auto make_const_pattern = [&]() {
+ return ColumnConst::create(make_str_col({"(b)"}), strs.size());
+ };
+
+ // std::nullopt marks a NULL row.
+ using Expected = std::vector<std::optional<std::string>>;
+ auto run_case = [&](const std::string& func_name, ColumnsWithTypeAndName
arg_cols,
+ const DataTypePtr& return_type, const Expected&
expected) {
+ Block block;
+ ColumnNumbers arguments;
+ std::vector<DataTypePtr> arg_types;
+ std::vector<std::shared_ptr<ColumnPtrWrapper>> constant_cols;
+ for (auto& arg : arg_cols) {
+ arguments.push_back(static_cast<unsigned int>(block.columns()));
+ arg_types.push_back(arg.type);
+ constant_cols.push_back(is_column_const(*arg.column)
+ ?
std::make_shared<ColumnPtrWrapper>(arg.column)
+ : nullptr);
+ block.insert(std::move(arg));
+ }
+ auto func = SimpleFunctionFactory::instance().get_function(
+ func_name, block.get_columns_with_type_and_name(),
return_type);
+ ASSERT_TRUE(func != nullptr) << func_name;
+
+ auto result = block.columns();
+ block.insert({nullptr, return_type, "result"});
+
+ FunctionUtils fn_utils({}, arg_types, false);
+ auto* fn_ctx = fn_utils.get_fn_ctx();
+ fn_ctx->set_constant_cols(constant_cols);
+
+ ASSERT_EQ(Status::OK(), func->open(fn_ctx,
FunctionContext::FRAGMENT_LOCAL)) << func_name;
+ ASSERT_EQ(Status::OK(), func->open(fn_ctx,
FunctionContext::THREAD_LOCAL)) << func_name;
+ ASSERT_EQ(Status::OK(), func->execute(fn_ctx, block, arguments,
result, strs.size()))
+ << func_name;
+
+ const auto& result_column = block.get_by_position(result).column;
+ ASSERT_TRUE(result_column->is_nullable()) << func_name;
+ ASSERT_EQ(expected.size(), result_column->size()) << func_name;
+ for (size_t i = 0; i < expected.size(); ++i) {
+ if (expected[i]) {
+ EXPECT_FALSE(result_column->is_null_at(i)) << func_name << "
row " << i;
+ EXPECT_EQ(*expected[i], return_type->to_string(*result_column,
i))
+ << func_name << " row " << i;
+ } else {
+ EXPECT_TRUE(result_column->is_null_at(i)) << func_name << "
row " << i;
+ }
+ }
+
+ static_cast<void>(func->close(fn_ctx, FunctionContext::THREAD_LOCAL));
+ static_cast<void>(func->close(fn_ctx,
FunctionContext::FRAGMENT_LOCAL));
+ };
+
+ auto nullable_array =
make_nullable(std::make_shared<DataTypeArray>(nullable_str));
+ for (bool const_pattern : {false, true}) {
+ // With a constant pattern the other parameters are constant too,
which takes the
+ // functions' all-constant-parameters path.
+ auto make_pattern = [&]() -> ColumnPtr {
+ return const_pattern ? ColumnPtr(make_const_pattern()) :
make_column_pattern();
+ };
+ auto make_index = [&]() -> ColumnPtr {
+ return const_pattern ?
ColumnPtr(ColumnConst::create(make_int_col({1}), strs.size()))
+ : make_int_col({1, 1});
+ };
+ auto make_replacement = [&]() -> ColumnPtr {
+ return const_pattern ?
ColumnPtr(ColumnConst::create(make_str_col({"x"}), strs.size()))
+ : make_str_col({"x", "x"});
+ };
+ run_case("regexp_extract",
+ {{make_str_col(strs), str_type, "str"},
+ {make_pattern(), str_type, "pattern"},
+ {make_index(), int_type, "idx"}},
+ nullable_str, {"b", ""});
+ run_case("regexp_extract_or_null",
+ {{make_str_col(strs), str_type, "str"},
+ {make_pattern(), str_type, "pattern"},
+ {make_index(), int_type, "idx"}},
+ nullable_str, {"b", std::nullopt});
+ run_case("regexp_extract_all",
+ {{make_str_col(strs), str_type, "str"}, {make_pattern(),
str_type, "pattern"}},
+ nullable_str, {"['b']", ""});
+ run_case("regexp_extract_all_array",
+ {{make_str_col(strs), str_type, "str"}, {make_pattern(),
str_type, "pattern"}},
+ nullable_array, {"[\"b\"]", "[]"});
+ for (const char* func_name : {"regexp_replace", "regexp_replace_one"})
{
+ run_case(func_name,
+ {{make_str_col(strs), str_type, "str"},
+ {make_pattern(), str_type, "pattern"},
+ {make_replacement(), str_type, "repl"}},
+ nullable_str, {"axc", "xyz"});
+ }
+ }
+}
+
// Enhanced tests for better coverage
TEST(FunctionLikeTest, pattern_optimization_allpass) {
diff --git
a/regression-test/data/query_p0/sql_functions/string_functions/test_regexp_invalid_pattern.out
b/regression-test/data/query_p0/sql_functions/string_functions/test_regexp_invalid_pattern.out
new file mode 100644
index 00000000000..1f8a7f56d8c
--- /dev/null
+++
b/regression-test/data/query_p0/sql_functions/string_functions/test_regexp_invalid_pattern.out
@@ -0,0 +1,98 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !extract_valid --
+1 b
+3 \N
+4 b
+
+-- !extract_or_null_valid --
+1 b
+3 \N
+4 b
+
+-- !extract_all_valid --
+1 ['b']
+3 \N
+4 ['b']
+
+-- !extract_all_array_valid --
+1 ["b"]
+3 \N
+4 ["b"]
+
+-- !replace_valid --
+1 axc
+3 \N
+4 axc
+
+-- !replace_one_valid --
+1 axc
+3 \N
+4 axc
+
+-- !count_valid --
+1 1
+3 \N
+4 1
+
+-- !extract_null_payload --
+1 b
+2 \N
+3 \N
+4 b
+
+-- !extract_or_null_null_payload --
+1 b
+2 \N
+3 \N
+4 b
+
+-- !extract_all_null_payload --
+1 ['b']
+2 \N
+3 \N
+4 ['b']
+
+-- !extract_all_array_null_payload --
+1 ["b"]
+2 \N
+3 \N
+4 ["b"]
+
+-- !replace_null_payload --
+1 axc
+2 \N
+3 \N
+4 axc
+
+-- !replace_one_null_payload --
+1 axc
+2 \N
+3 \N
+4 axc
+
+-- !count_null_payload --
+1 1
+2 \N
+3 \N
+4 1
+
+-- !extract_null_const --
+1 \N \N
+
+-- !extract_all_null_const --
+1 \N \N
+
+-- !replace_null_const --
+1 \N \N
+
+-- !count_null_const --
+1 \N \N
+
+-- !not_null_column_pattern --
+1 b b ['b'] ["b"] axc axc 1
+2 \N [] xyz xyz 0
+
+-- !not_null_const_pattern --
+1 b b ['b'] ["b"] axc axc 1
+2 \N [] xyz xyz 0
+
diff --git
a/regression-test/suites/query_p0/sql_functions/string_functions/test_regexp_invalid_pattern.groovy
b/regression-test/suites/query_p0/sql_functions/string_functions/test_regexp_invalid_pattern.groovy
new file mode 100644
index 00000000000..e87cdbb1da2
--- /dev/null
+++
b/regression-test/suites/query_p0/sql_functions/string_functions/test_regexp_invalid_pattern.groovy
@@ -0,0 +1,173 @@
+// 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.
+
+// An invalid regex pattern must raise the same error whether it is a constant
+// or read from a column; it must never be silently turned into NULL.
+suite("test_regexp_invalid_pattern") {
+ sql "DROP TABLE IF EXISTS test_regexp_invalid_pattern"
+ sql """
+ CREATE TABLE test_regexp_invalid_pattern (
+ id INT,
+ s STRING,
+ p STRING,
+ repl STRING
+ ) DUPLICATE KEY(id) DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES("replication_num" = "1");
+ """
+ sql """
+ INSERT INTO test_regexp_invalid_pattern VALUES
+ (1, 'abc', '(b)', 'x'),
+ (2, 'abc', '[', 'x'),
+ (3, 'abc', NULL, 'x'),
+ (4, 'abc', '(b)', 'x');
+ """
+
+ // Valid column patterns keep working, a NULL pattern between them still
yields NULL.
+ order_qt_extract_valid "SELECT id, regexp_extract(s, p, 1) FROM
test_regexp_invalid_pattern WHERE id IN (1, 3, 4)"
+ order_qt_extract_or_null_valid "SELECT id, regexp_extract_or_null(s, p, 1)
FROM test_regexp_invalid_pattern WHERE id IN (1, 3, 4)"
+ order_qt_extract_all_valid "SELECT id, regexp_extract_all(s, p) FROM
test_regexp_invalid_pattern WHERE id IN (1, 3, 4)"
+ order_qt_extract_all_array_valid "SELECT id, regexp_extract_all_array(s,
p) FROM test_regexp_invalid_pattern WHERE id IN (1, 3, 4)"
+ order_qt_replace_valid "SELECT id, regexp_replace(s, p, repl) FROM
test_regexp_invalid_pattern WHERE id IN (1, 3, 4)"
+ order_qt_replace_one_valid "SELECT id, regexp_replace_one(s, p, repl) FROM
test_regexp_invalid_pattern WHERE id IN (1, 3, 4)"
+ order_qt_count_valid "SELECT id, regexp_count(s, p) FROM
test_regexp_invalid_pattern WHERE id IN (1, 3, 4)"
+
+ // Constant invalid pattern: rejected.
+ test {
+ sql "SELECT regexp_extract('abc', '[', 0)"
+ exception "Invalid regex pattern"
+ }
+ test {
+ sql "SELECT regexp_extract_or_null('abc', '[', 0)"
+ exception "Invalid regex pattern"
+ }
+ test {
+ sql "SELECT regexp_extract_all('abc', '[')"
+ exception "Invalid regex pattern"
+ }
+ test {
+ sql "SELECT regexp_extract_all_array('abc', '[')"
+ exception "Invalid regex pattern"
+ }
+ test {
+ sql "SELECT regexp_replace('abc', '[', 'x')"
+ exception "Could not compile regexp pattern"
+ }
+ test {
+ sql "SELECT regexp_replace_one('abc', '[', 'x')"
+ exception "Could not compile regexp pattern"
+ }
+ test {
+ sql "SELECT regexp_count('abc', '[')"
+ exception "Could not compile regexp pattern"
+ }
+
+ // Column invalid pattern: rejected with the same error instead of
returning NULL.
+ test {
+ sql "SELECT regexp_extract(s, p, 0) FROM test_regexp_invalid_pattern"
+ exception "Invalid regex pattern"
+ }
+ test {
+ sql "SELECT regexp_extract_or_null(s, p, 0) FROM
test_regexp_invalid_pattern"
+ exception "Invalid regex pattern"
+ }
+ test {
+ sql "SELECT regexp_extract_all(s, p) FROM test_regexp_invalid_pattern"
+ exception "Invalid regex pattern"
+ }
+ test {
+ sql "SELECT regexp_extract_all_array(s, p) FROM
test_regexp_invalid_pattern"
+ exception "Invalid regex pattern"
+ }
+ test {
+ sql "SELECT regexp_replace(s, p, repl) FROM
test_regexp_invalid_pattern"
+ exception "Could not compile regexp pattern"
+ }
+ test {
+ sql "SELECT regexp_replace_one(s, p, repl) FROM
test_regexp_invalid_pattern"
+ exception "Could not compile regexp pattern"
+ }
+ test {
+ sql "SELECT regexp_count(s, p) FROM test_regexp_invalid_pattern"
+ exception "Could not compile regexp pattern"
+ }
+
+ // A SQL NULL pattern yields NULL for that row even when the bytes left
under the NULL
+ // slot (here the invalid '[' of row 2, kept by concat) do not compile.
+ order_qt_extract_null_payload "SELECT id, regexp_extract(s, concat(p,
if(id = 2, NULL, '')), 1) FROM test_regexp_invalid_pattern"
+ order_qt_extract_or_null_null_payload "SELECT id,
regexp_extract_or_null(s, concat(p, if(id = 2, NULL, '')), 1) FROM
test_regexp_invalid_pattern"
+ order_qt_extract_all_null_payload "SELECT id, regexp_extract_all(s,
concat(p, if(id = 2, NULL, ''))) FROM test_regexp_invalid_pattern"
+ order_qt_extract_all_array_null_payload "SELECT id,
regexp_extract_all_array(s, concat(p, if(id = 2, NULL, ''))) FROM
test_regexp_invalid_pattern"
+ order_qt_replace_null_payload "SELECT id, regexp_replace(s, concat(p,
if(id = 2, NULL, '')), repl) FROM test_regexp_invalid_pattern"
+ order_qt_replace_one_null_payload "SELECT id, regexp_replace_one(s,
concat(p, if(id = 2, NULL, '')), repl) FROM test_regexp_invalid_pattern"
+ order_qt_count_null_payload "SELECT id, regexp_count(s, concat(p, if(id =
2, NULL, ''))) FROM test_regexp_invalid_pattern"
+
+ // NULL constant pattern and NULL input string keep propagating NULL.
+ order_qt_extract_null_const "SELECT id, regexp_extract(s, NULL, 1),
regexp_extract(NULL, p, 1) FROM test_regexp_invalid_pattern WHERE id = 1"
+ order_qt_extract_all_null_const "SELECT id, regexp_extract_all(s, NULL),
regexp_extract_all_array(NULL, p) FROM test_regexp_invalid_pattern WHERE id = 1"
+ order_qt_replace_null_const "SELECT id, regexp_replace(s, NULL, repl),
regexp_replace_one(s, p, NULL) FROM test_regexp_invalid_pattern WHERE id = 1"
+ order_qt_count_null_const "SELECT id, regexp_count(s, NULL),
regexp_count(NULL, p) FROM test_regexp_invalid_pattern WHERE id = 1"
+
+ // Constant string with column pattern is a column path too.
+ test {
+ sql "SELECT regexp_extract('abc', p, 0) FROM
test_regexp_invalid_pattern WHERE id = 2"
+ exception "Invalid regex pattern"
+ }
+ test {
+ sql "SELECT regexp_replace('abc', p, 'x') FROM
test_regexp_invalid_pattern WHERE id = 2"
+ exception "Could not compile regexp pattern"
+ }
+
+ // NOT NULL arguments run without an input NULL map: the results keep
their Nullable type,
+ // regexp_extract_or_null still returns NULL on no match, and an invalid
column pattern is
+ // still rejected.
+ sql "DROP TABLE IF EXISTS test_regexp_invalid_pattern_not_null"
+ sql """
+ CREATE TABLE test_regexp_invalid_pattern_not_null (
+ id INT NOT NULL,
+ s STRING NOT NULL,
+ p STRING NOT NULL,
+ repl STRING NOT NULL
+ ) DUPLICATE KEY(id) DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES("replication_num" = "1");
+ """
+ sql """
+ INSERT INTO test_regexp_invalid_pattern_not_null VALUES
+ (1, 'abc', '(b)', 'x'),
+ (2, 'xyz', '(q)', 'x');
+ """
+ order_qt_not_null_column_pattern """
+ SELECT id, regexp_extract(s, p, 1), regexp_extract_or_null(s, p, 1),
+ regexp_extract_all(s, p), regexp_extract_all_array(s, p),
+ regexp_replace(s, p, repl), regexp_replace_one(s, p, repl),
regexp_count(s, p)
+ FROM test_regexp_invalid_pattern_not_null
+ """
+ order_qt_not_null_const_pattern """
+ SELECT id, regexp_extract(s, '(b)', 1), regexp_extract_or_null(s,
'(b)', 1),
+ regexp_extract_all(s, '(b)'), regexp_extract_all_array(s,
'(b)'),
+ regexp_replace(s, '(b)', 'x'), regexp_replace_one(s, '(b)',
'x'),
+ regexp_count(s, '(b)')
+ FROM test_regexp_invalid_pattern_not_null
+ """
+ test {
+ sql "SELECT regexp_extract(s, concat(p, '['), 1) FROM
test_regexp_invalid_pattern_not_null"
+ exception "Invalid regex pattern"
+ }
+ test {
+ sql "SELECT regexp_replace(s, concat(p, '['), repl) FROM
test_regexp_invalid_pattern_not_null"
+ exception "Could not compile regexp pattern"
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]