This is an automated email from the ASF dual-hosted git repository. morningman pushed a commit to branch branch-1.2-lts in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-1.2-lts by this push: new 687d9d3300 [feature](function)Support negative index for function split_part (#13914) 687d9d3300 is described below commit 687d9d3300d3d336ddb739094c1b7764907418bf Author: Yulei-Yang <yulei.yang0...@gmail.com> AuthorDate: Mon Dec 12 09:56:09 2022 +0800 [feature](function)Support negative index for function split_part (#13914) --- be/src/exprs/string_functions.cpp | 107 ++++++++-- be/src/vec/functions/function_string.h | 221 +++++++++++++-------- be/test/exprs/string_functions_test.cpp | 49 +++++ be/test/vec/function/function_string_test.cpp | 17 -- .../sql-functions/string-functions/split_part.md | 33 ++- .../sql-functions/string-functions/split_part.md | 31 ++- .../string_functions/test_string_function.out | 33 +++ .../string_functions/test_string_function.groovy | 15 +- 8 files changed, 380 insertions(+), 126 deletions(-) diff --git a/be/src/exprs/string_functions.cpp b/be/src/exprs/string_functions.cpp index b980ad4a7f..0db6741f05 100644 --- a/be/src/exprs/string_functions.cpp +++ b/be/src/exprs/string_functions.cpp @@ -920,35 +920,98 @@ static int index_of(const uint8_t* source, int source_offset, int source_count, return -1; } +static int last_index_of(const uint8_t* source, int source_len, const uint8_t* target, + int target_len, int to_index) { + if (to_index < 0) { + return -1; + } + if (to_index >= source_len) { + to_index = source_len - 1; + } + if (target_len == 0) { + return to_index; + } + const uint8_t last = target[target_len - 1]; + int min = target_len; + for (int i = to_index; i >= min; i--) { + while (i >= min && source[i] != last) { + i--; // Look for last character + } + if (i >= min) { // Found first character, now look at the rest of v2 + int j = i - 1; + int end = j - target_len + 1; + for (int k = target_len - 2; j > end && source[j] == target[k];) { + j--; + k--; + } + if (j == end) { + return i - target_len + 1; + } + } + } + return -1; +} + StringVal StringFunctions::split_part(FunctionContext* context, const StringVal& content, const StringVal& delimiter, const IntVal& field) { - if (content.is_null || delimiter.is_null || field.is_null || field.val <= 0) { + if (content.is_null || delimiter.is_null || field.is_null || field.val == 0) { return StringVal::null(); } - std::vector<int> find(field.val, -1); //store substring position - int from = 0; - for (int i = 1; i <= field.val; i++) { // find - int last_index = i - 1; - find[last_index] = - index_of(content.ptr, 0, content.len, delimiter.ptr, 0, delimiter.len, from); - from = find[last_index] + delimiter.len; - if (find[last_index] == -1) { - break; + + if (field.val > 0) { + int from = 0; + std::vector<int> find(field.val, -1); //store substring position + for (int i = 1; i <= field.val; i++) { // find + int last_index = i - 1; + find[last_index] = + index_of(content.ptr, 0, content.len, delimiter.ptr, 0, delimiter.len, from); + from = find[last_index] + delimiter.len; + if (find[last_index] == -1) { + break; + } } - } - if ((field.val > 1 && find[field.val - 2] == -1) || - (field.val == 1 && find[field.val - 1] == -1)) { - // field not find return null - return StringVal::null(); - } - int start_pos; - if (field.val == 1) { // find need split first part - start_pos = 0; + if ((field.val > 1 && find[field.val - 2] == -1) || + (field.val == 1 && find[field.val - 1] == -1)) { + // field not find return null + return StringVal::null(); + } + int start_pos; + if (field.val == 1) { // find need split first part + start_pos = 0; + } else { + start_pos = find[field.val - 2] + delimiter.len; + } + int len = (find[field.val - 1] == -1 ? content.len : find[field.val - 1]) - start_pos; + return StringVal(content.ptr + start_pos, len); } else { - start_pos = find[field.val - 2] + delimiter.len; + int to = content.len; + int abs_field = -field.val; + std::vector<int> find(abs_field, -1); //store substring position + for (int i = 1; i <= abs_field; i++) { // find + int last_index = i - 1; + find[last_index] = + last_index_of(content.ptr, content.len, delimiter.ptr, delimiter.len, to); + to = find[last_index] - delimiter.len; + if (find[last_index] == -1) { + break; + } + } + if ((abs_field > 1 && find[abs_field - 2] == -1) || + (abs_field == 1 && find[abs_field - 1] == -1)) { + // field not find return null + return StringVal::null(); + } + int end_pos; + if (abs_field == 1) { // find need split first part + end_pos = content.len - 1; + } else { + end_pos = find[abs_field - 2] - 1; + } + int len = + end_pos - (find[abs_field - 1] == -1 ? 0 : find[abs_field - 1] + delimiter.len) + 1; + + return StringVal(content.ptr + end_pos - len + 1, len); } - int len = (find[field.val - 1] == -1 ? content.len : find[field.val - 1]) - start_pos; - return StringVal(content.ptr + start_pos, len); } StringVal StringFunctions::replace(FunctionContext* context, const StringVal& origStr, diff --git a/be/src/vec/functions/function_string.h b/be/src/vec/functions/function_string.h index 01760840b7..73089500a6 100644 --- a/be/src/vec/functions/function_string.h +++ b/be/src/vec/functions/function_string.h @@ -1234,8 +1234,8 @@ public: return make_nullable(std::make_shared<DataTypeString>()); } - bool use_default_implementation_for_nulls() const override { return false; } - bool use_default_implementation_for_constants() const override { return true; } + bool use_default_implementation_for_nulls() const override { return true; } + bool use_default_implementation_for_constants() const override { return false; } Status execute_impl(FunctionContext* context, Block& block, const ColumnNumbers& arguments, size_t result, size_t input_rows_count) override { @@ -1251,97 +1251,151 @@ public: auto& res_chars = res->get_chars(); res_offsets.resize(input_rows_count); - size_t argument_size = arguments.size(); - ColumnPtr argument_columns[argument_size]; - for (size_t i = 0; i < argument_size; ++i) { - argument_columns[i] = - block.get_by_position(arguments[i]).column->convert_to_full_column_if_const(); - if (auto* nullable = check_and_get_column<const ColumnNullable>(*argument_columns[i])) { - // Danger: Here must dispose the null map data first! Because - // argument_columns[i]=nullable->get_nested_column_ptr(); will release the mem - // of column nullable mem of null map - VectorizedUtils::update_null_map(null_map->get_data(), - nullable->get_null_map_data()); - argument_columns[i] = nullable->get_nested_column_ptr(); + ColumnPtr content_column = + block.get_by_position(arguments[0]).column->convert_to_full_column_if_const(); + + if (auto* nullable = check_and_get_column<const ColumnNullable>(*content_column)) { + // Danger: Here must dispose the null map data first! Because + // argument_columns[0]=nullable->get_nested_column_ptr(); will release the mem + // of column nullable mem of null map + VectorizedUtils::update_null_map(null_map->get_data(), nullable->get_null_map_data()); + content_column = nullable->get_nested_column_ptr(); + } + + for (size_t i = 1; i <= 2; i++) { + ColumnPtr columnPtr = remove_nullable(block.get_by_position(arguments[i]).column); + + if (!is_column_const(*columnPtr)) { + return Status::RuntimeError("Argument at index {} for function {} must be constant", + i + 1, get_name()); } } - auto str_col = assert_cast<const ColumnString*>(argument_columns[0].get()); + auto str_col = assert_cast<const ColumnString*>(content_column.get()); - auto delimiter_col = assert_cast<const ColumnString*>(argument_columns[1].get()); + const IColumn& delimiter_col = *block.get_by_position(arguments[1]).column; + const auto* delimiter_const = typeid_cast<const ColumnConst*>(&delimiter_col); + auto delimiter = delimiter_const->get_field().get<String>(); + int32_t delimiter_size = delimiter.size(); - auto part_num_col = assert_cast<const ColumnInt32*>(argument_columns[2].get()); - auto& part_num_col_data = part_num_col->get_data(); + const IColumn& part_num_col = *block.get_by_position(arguments[2]).column; + const auto* part_num_col_const = typeid_cast<const ColumnConst*>(&part_num_col); + auto part_number = part_num_col_const->get_field().get<Int32>(); - for (size_t i = 0; i < input_rows_count; ++i) { - if (part_num_col_data[i] <= 0) { - StringOP::push_null_string(i, res_chars, res_offsets, null_map_data); - continue; - } + if (part_number >= 0) { + for (size_t i = 0; i < input_rows_count; ++i) { + if (part_number == 0) { + StringOP::push_null_string(i, res_chars, res_offsets, null_map_data); + continue; + } - auto delimiter = delimiter_col->get_data_at(i); - auto delimiter_str = delimiter_col->get_data_at(i).to_string(); - auto part_number = part_num_col_data[i]; - auto str = str_col->get_data_at(i); - if (delimiter.size == 0) { - StringOP::push_empty_string(i, res_chars, res_offsets); - } else if (delimiter.size == 1) { - // If delimiter is a char, use memchr to split - int32_t pre_offset = -1; - int32_t offset = -1; - int32_t num = 0; - while (num < part_number) { - pre_offset = offset; - size_t n = str.size - offset - 1; - const char* pos = reinterpret_cast<const char*>( - memchr(str.data + offset + 1, delimiter_str[0], n)); - if (pos != nullptr) { - offset = pos - str.data; - num++; - } else { - offset = str.size; - num = (num == 0) ? 0 : num + 1; - break; + auto str = str_col->get_data_at(i); + if (delimiter_size == 0) { + StringOP::push_empty_string(i, res_chars, res_offsets); + } else if (delimiter_size == 1) { + // If delimiter is a char, use memchr to split + int32_t pre_offset = -1; + int32_t offset = -1; + int32_t num = 0; + while (num < part_number) { + pre_offset = offset; + size_t n = str.size - offset - 1; + const char* pos = reinterpret_cast<const char*>( + memchr(str.data + offset + 1, delimiter[0], n)); + if (pos != nullptr) { + offset = pos - str.data; + num++; + } else { + offset = str.size; + num = (num == 0) ? 0 : num + 1; + break; + } } - } - if (num == part_number) { - StringOP::push_value_string( - std::string_view { - reinterpret_cast<const char*>(str.data + pre_offset + 1), - (size_t)offset - pre_offset - 1}, - i, res_chars, res_offsets); + if (num == part_number) { + StringOP::push_value_string( + std::string_view { + reinterpret_cast<const char*>(str.data + pre_offset + 1), + (size_t)offset - pre_offset - 1}, + i, res_chars, res_offsets); + } else { + StringOP::push_null_string(i, res_chars, res_offsets, null_map_data); + } } else { - StringOP::push_null_string(i, res_chars, res_offsets, null_map_data); - } - } else { - // If delimiter is a string, use memmem to split - int32_t pre_offset = -delimiter.size; - int32_t offset = -delimiter.size; - int32_t num = 0; - while (num < part_number) { - pre_offset = offset; - size_t n = str.size - offset - delimiter.size; - char* pos = reinterpret_cast<char*>(memmem(str.data + offset + delimiter.size, - n, delimiter.data, delimiter.size)); - if (pos != nullptr) { - offset = pos - str.data; - num++; + // If delimiter is a string, use memmem to split + int32_t pre_offset = -delimiter_size; + int32_t offset = pre_offset; + int32_t num = 0; + while (num < part_number) { + pre_offset = offset; + size_t n = str.size - offset - delimiter_size; + char* pos = reinterpret_cast<char*>( + memmem(str.data + offset + delimiter_size, n, delimiter.c_str(), + delimiter_size)); + if (pos != nullptr) { + offset = pos - str.data; + num++; + } else { + offset = str.size; + num = (num == 0) ? 0 : num + 1; + break; + } + } + + if (num == part_number) { + StringOP::push_value_string( + std::string_view {reinterpret_cast<const char*>( + str.data + pre_offset + delimiter_size), + (size_t)offset - pre_offset - delimiter_size}, + i, res_chars, res_offsets); } else { - offset = str.size; - num = (num == 0) ? 0 : num + 1; - break; + StringOP::push_null_string(i, res_chars, res_offsets, null_map_data); } } - - if (num == part_number) { - StringOP::push_value_string( - std::string_view {reinterpret_cast<const char*>(str.data + pre_offset + - delimiter.size), - (size_t)offset - pre_offset - delimiter.size}, - i, res_chars, res_offsets); + } + } else { + part_number = -part_number; + for (size_t i = 0; i < input_rows_count; ++i) { + if (delimiter_size == 0) { + StringOP::push_empty_string(i, res_chars, res_offsets); } else { - StringOP::push_null_string(i, res_chars, res_offsets, null_map_data); + auto str = str_col->get_data_at(i); + auto str_str = str.to_string(); + int32_t offset = str.size; + int32_t pre_offset = offset; + int32_t num = 0; + auto substr = str_str; + while (num <= part_number && offset >= 0) { + offset = (int)substr.rfind(delimiter, offset); + if (offset != -1) { + if (++num == part_number) { + break; + } + pre_offset = offset; + offset = offset - 1; + substr = str_str.substr(0, pre_offset); + } else { + break; + } + } + num = (offset == -1 && num != 0) ? num + 1 : num; + + if (num == part_number) { + if (offset == -1) { + StringOP::push_value_string( + std::string_view {reinterpret_cast<const char*>(str.data), + (size_t)pre_offset}, + i, res_chars, res_offsets); + } else { + StringOP::push_value_string( + std::string_view {str_str.substr( + offset + delimiter_size, + (size_t)pre_offset - offset - delimiter_size)}, + i, res_chars, res_offsets); + } + } else { + StringOP::push_null_string(i, res_chars, res_offsets, null_map_data); + } } } } @@ -1717,10 +1771,11 @@ public: if (!success) { // url is malformed, or url_part is invalid. if (url_part == UrlParser::INVALID) { - return Status::RuntimeError( - "Invalid URL part: {}\n{}", std::string(part.data, part.size), - "(Valid URL parts are 'PROTOCOL', 'HOST', 'PATH', 'REF', 'AUTHORITY', " - "'FILE', 'USERINFO', 'PORT' and 'QUERY')"); + return Status::RuntimeError("Invalid URL part: {}\n{}", + std::string(part.data, part.size), + "(Valid URL parts are 'PROTOCOL', 'HOST', " + "'PATH', 'REF', 'AUTHORITY', " + "'FILE', 'USERINFO', 'PORT' and 'QUERY')"); } else { StringOP::push_null_string(i, res_chars, res_offsets, null_map_data); continue; diff --git a/be/test/exprs/string_functions_test.cpp b/be/test/exprs/string_functions_test.cpp index fa6d912b4e..89c58ecfb3 100644 --- a/be/test/exprs/string_functions_test.cpp +++ b/be/test/exprs/string_functions_test.cpp @@ -187,6 +187,55 @@ TEST_F(StringFunctionsTest, split_part) { AnyValUtil::from_string(ctx, std::string("#123")), StringFunctions::split_part(context, StringVal("abc###123###234"), StringVal("##"), 2)); + EXPECT_EQ(AnyValUtil::from_string(ctx, std::string("234")), + StringFunctions::split_part(context, StringVal("abc###123###234"), StringVal("##"), + -1)); + + EXPECT_EQ(AnyValUtil::from_string(ctx, std::string("123#")), + StringFunctions::split_part(context, StringVal("abc###123###234"), StringVal("##"), + -2)); + + EXPECT_EQ(AnyValUtil::from_string(ctx, std::string("abc#")), + StringFunctions::split_part(context, StringVal("abc###123###234"), StringVal("##"), + -3)); + + EXPECT_EQ(StringVal::null(), StringFunctions::split_part(context, StringVal("abc###123###234"), + StringVal("##"), -4)); + + EXPECT_EQ(AnyValUtil::from_string(ctx, std::string("234")), + StringFunctions::split_part(context, StringVal("abc#123##234"), StringVal("#"), -1)); + + EXPECT_EQ(AnyValUtil::from_string(ctx, std::string("")), + StringFunctions::split_part(context, StringVal("abc#123##234"), StringVal("#"), -2)); + + EXPECT_EQ(AnyValUtil::from_string(ctx, std::string("123")), + StringFunctions::split_part(context, StringVal("abc#123##234"), StringVal("#"), -3)); + + EXPECT_EQ(AnyValUtil::from_string(ctx, std::string("abc")), + StringFunctions::split_part(context, StringVal("abc#123##234"), StringVal("#"), -4)); + + EXPECT_EQ(StringVal::null(), + StringFunctions::split_part(context, StringVal("abc#123##234"), StringVal("#"), -5)); + + EXPECT_EQ(StringVal::null(), StringFunctions::split_part(context, StringVal("abc#123##234"), + StringVal("#"), IntVal::null())); + + EXPECT_EQ(StringVal::null(), StringFunctions::split_part(context, StringVal("abc#123##234"), + StringVal::null(), -1)); + + EXPECT_EQ( + AnyValUtil::from_string(ctx, std::string("")), + StringFunctions::split_part(context, StringVal("2019年9月-12月"), StringVal("月"), -1)); + EXPECT_EQ( + AnyValUtil::from_string(ctx, std::string("-12")), + StringFunctions::split_part(context, StringVal("2019年9月-12月"), StringVal("月"), -2)); + + EXPECT_EQ( + AnyValUtil::from_string(ctx, std::string("2019年9")), + StringFunctions::split_part(context, StringVal("2019年9月-12月"), StringVal("月"), -3)); + + EXPECT_EQ(StringVal::null(), StringFunctions::split_part(context, StringVal("2019年9月-12月"), + StringVal("月"), -4)); delete context; } diff --git a/be/test/vec/function/function_string_test.cpp b/be/test/vec/function/function_string_test.cpp index e996bb13f9..c499718ff1 100644 --- a/be/test/vec/function/function_string_test.cpp +++ b/be/test/vec/function/function_string_test.cpp @@ -571,23 +571,6 @@ TEST(function_string_test, function_find_in_set_test) { check_function<DataTypeInt32, true>(func_name, input_types, data_set); } -TEST(function_string_test, function_string_splitpart_test) { - std::string func_name = "split_part"; - InputTypeSet input_types = {TypeIndex::String, TypeIndex::String, TypeIndex::Int32}; - - DataSet data_set = { - {{std::string("prefix_string1"), std::string("_"), 2}, std::string("string1")}, - {{std::string("prefix__string2"), std::string("__"), 2}, std::string("string2")}, - {{std::string("prefix__string2"), std::string("_"), 2}, std::string("")}, - {{std::string("prefix_string2"), std::string("__"), 1}, Null()}, - {{Null(), std::string("__"), 1}, Null()}, - {{std::string("prefix_string"), Null(), 1}, Null()}, - {{std::string("prefix_string"), std::string("__"), Null()}, Null()}, - {{std::string("prefix_string"), std::string("__"), -1}, Null()}}; - - check_function<DataTypeString, true>(func_name, input_types, data_set); -} - TEST(function_string_test, function_md5sum_test) { std::string func_name = "md5sum"; diff --git a/docs/en/docs/sql-manual/sql-functions/string-functions/split_part.md b/docs/en/docs/sql-manual/sql-functions/string-functions/split_part.md index 67a7bc721a..818cf6c9dc 100644 --- a/docs/en/docs/sql-manual/sql-functions/string-functions/split_part.md +++ b/docs/en/docs/sql-manual/sql-functions/string-functions/split_part.md @@ -31,7 +31,9 @@ under the License. `VARCHAR split party (VARCHAR content, VARCHAR delimiter, INT field)` -Returns the specified partition (counting from the beginning) by splitting the string according to the partitioner. +Returns the specified partition by splitting the string according to the delimiter. If field is positive, splitting and counting from the beginning of content, otherwise from the ending. + +`delimiter` and `field` parameter should be constant. ### example @@ -64,6 +66,35 @@ mysql> select split_part("abca", "a", 1); +----------------------------+ | | +----------------------------+ + +mysql> select split_part("prefix_string", "_", -1); ++--------------------------------------+ +| split_part('prefix_string', '_', -1) | ++--------------------------------------+ +| string | ++--------------------------------------+ + + +mysql> select split_part("prefix_string", "_", -2); ++--------------------------------------+ +| split_part('prefix_string', '_', -2) | ++--------------------------------------+ +| prefix | ++--------------------------------------+ + +mysql> select split_part("abc##123###234", "##", -1); ++----------------------------------------+ +| split_part('abc##123###234', '##', -1) | ++----------------------------------------+ +| 234 | ++----------------------------------------+ + +mysql> select split_part("abc##123###234", "##", -2); ++----------------------------------------+ +| split_part('abc##123###234', '##', -2) | ++----------------------------------------+ +| 123# | ++----------------------------------------+ ``` ### keywords SPLIT_PART,SPLIT,PART diff --git a/docs/zh-CN/docs/sql-manual/sql-functions/string-functions/split_part.md b/docs/zh-CN/docs/sql-manual/sql-functions/string-functions/split_part.md index 00e5620f53..0291ae29ca 100644 --- a/docs/zh-CN/docs/sql-manual/sql-functions/string-functions/split_part.md +++ b/docs/zh-CN/docs/sql-manual/sql-functions/string-functions/split_part.md @@ -31,7 +31,8 @@ under the License. `VARCHAR split_part(VARCHAR content, VARCHAR delimiter, INT field)` -根据分割符拆分字符串, 返回指定的分割部分(从一开始计数)。 +根据分割符拆分字符串, 返回指定的分割部分(从一或负一开始计数)。field字段支持负数,代表从右往左倒着截取并取数。 +`delimiter` 和 `field` 参数需要是常量, 不支持变量。 ### example @@ -64,6 +65,34 @@ mysql> select split_part("abca", "a", 1); +----------------------------+ | | +----------------------------+ + +mysql> select split_part("prefix_string", "_", -1); ++--------------------------------------+ +| split_part('prefix_string', '_', -1) | ++--------------------------------------+ +| string | ++--------------------------------------+ + +mysql> select split_part("prefix_string", "_", -2); ++--------------------------------------+ +| split_part('prefix_string', '_', -2) | ++--------------------------------------+ +| prefix | ++--------------------------------------+ + +mysql> select split_part("abc##123###234", "##", -1); ++----------------------------------------+ +| split_part('abc##123###234', '##', -1) | ++----------------------------------------+ +| 234 | ++----------------------------------------+ + +mysql> select split_part("abc##123###234", "##", -2); ++----------------------------------------+ +| split_part('abc##123###234', '##', -2) | ++----------------------------------------+ +| 123# | ++----------------------------------------+ ``` ### keywords SPLIT_PART,SPLIT,PART diff --git a/regression-test/data/query_p0/sql_functions/string_functions/test_string_function.out b/regression-test/data/query_p0/sql_functions/string_functions/test_string_function.out index 8b2a4c8bb0..2ef20aff92 100644 --- a/regression-test/data/query_p0/sql_functions/string_functions/test_string_function.out +++ b/regression-test/data/query_p0/sql_functions/string_functions/test_string_function.out @@ -224,6 +224,39 @@ hello -- !sql -- world +-- !sql -- +\\N + +-- !sql -- +world + +-- !sql -- +hello + +-- !sql -- +\\N + +-- !sql -- +\\N + +-- !sql -- +abc + +-- !sql -- +#xyz + +-- !sql -- +\\N + +-- !sql -- +xyz + +-- !sql -- +123# + +-- !sql -- +\\N + -- !sql -- true diff --git a/regression-test/suites/query_p0/sql_functions/string_functions/test_string_function.groovy b/regression-test/suites/query_p0/sql_functions/string_functions/test_string_function.groovy index fb0df8f38f..7c78210608 100644 --- a/regression-test/suites/query_p0/sql_functions/string_functions/test_string_function.groovy +++ b/regression-test/suites/query_p0/sql_functions/string_functions/test_string_function.groovy @@ -117,8 +117,19 @@ suite("test_string_function") { qt_sql "SELECT REVERSE('hello');" - qt_sql "select split_part(\"hello world\", \" \", 1);" - qt_sql "select split_part(\"hello world\", \" \", 2);" + qt_sql "select split_part('hello world', ' ', 1)" + qt_sql "select split_part('hello world', ' ', 2)" + qt_sql "select split_part('hello world', ' ', 0)" + qt_sql "select split_part('hello world', ' ', -1)" + qt_sql "select split_part('hello world', ' ', -2)" + qt_sql "select split_part('hello world', ' ', -3)" + qt_sql "select split_part('abc##123###xyz', '##', 0)" + qt_sql "select split_part('abc##123###xyz', '##', 1)" + qt_sql "select split_part('abc##123###xyz', '##', 3)" + qt_sql "select split_part('abc##123###xyz', '##', 5)" + qt_sql "select split_part('abc##123###xyz', '##', -1)" + qt_sql "select split_part('abc##123###xyz', '##', -2)" + qt_sql "select split_part('abc##123###xyz', '##', -4)" qt_sql "select starts_with(\"hello world\",\"hello\");" qt_sql "select starts_with(\"hello world\",\"world\");" --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@doris.apache.org For additional commands, e-mail: commits-h...@doris.apache.org