From 3d08dd702a620d59d5307d94e2da74b72b8ca199 Mon Sep 17 00:00:00 2001 From: Ma-Jian1 Date: Wed, 26 Jul 2023 10:04:49 +0800 Subject: [PATCH 1/4] support spark str_to_map --- velox/docs/functions/spark/string.rst | 13 +++ velox/functions/sparksql/Register.cpp | 8 ++ velox/functions/sparksql/StringToMap.h | 103 ++++++++++++++++++ velox/functions/sparksql/tests/CMakeLists.txt | 1 + .../sparksql/tests/StringToMapTest.cpp | 66 +++++++++++ 5 files changed, 191 insertions(+) create mode 100644 velox/functions/sparksql/StringToMap.h create mode 100644 velox/functions/sparksql/tests/StringToMapTest.cpp diff --git a/velox/docs/functions/spark/string.rst b/velox/docs/functions/spark/string.rst index 2959943cb5c5..8c3d9d2f9d99 100644 --- a/velox/docs/functions/spark/string.rst +++ b/velox/docs/functions/spark/string.rst @@ -178,6 +178,19 @@ Unless specified otherwise, all functions return NULL if at least one of the arg SELECT startswith('js SQL', 'SQL'); -- false SELECT startswith('js SQL', null); -- NULL +.. spark:function:: str_to_map(string, entryDelim, keyValueDelim) -> map(string, string) + + Returns a map by splitting ``string`` into entries with ``entryDelim`` and splitting + each entry into key/value with ``keyValueDelim``. + Only supports constant single-character ``entryDelim`` and ``keyValueDelim``. Throws + exception when duplicate map keys are found for single row's result, consistent with + Spark's default behavior. :: + + SELECT str_to_map('a:1,b:2,c:3', ',', ':'); -- {"a":"1","b":"2","c":"3"} + SELECT str_to_map('a', ',', ':'); -- {'a':NULL} + SELECT str_to_map('', ',', ':'); -- {'':NULL} + SELECT str_to_map('a:1,b:2,c:3', ',', ','); -- {"a:1":NULL,"b:2":NULL,"c:3":NULL} + .. spark:function:: substring(string, start) -> varchar Returns the rest of ``string`` from the starting position ``start``. diff --git a/velox/functions/sparksql/Register.cpp b/velox/functions/sparksql/Register.cpp index fb896fd29e75..98694da86cc1 100644 --- a/velox/functions/sparksql/Register.cpp +++ b/velox/functions/sparksql/Register.cpp @@ -36,6 +36,7 @@ #include "velox/functions/sparksql/RegisterCompare.h" #include "velox/functions/sparksql/Size.h" #include "velox/functions/sparksql/String.h" +#include "velox/functions/sparksql/StringToMap.h" #include "velox/functions/sparksql/UnscaledValueFunction.h" #include "velox/functions/sparksql/specialforms/DecimalRound.h" #include "velox/functions/sparksql/specialforms/MakeDecimal.h" @@ -153,6 +154,13 @@ void registerFunctions(const std::string& prefix) { int32_t, int32_t>({prefix + "overlay"}); + registerFunction< + sparksql::StringToMapFunction, + Map, + Varchar, + Varchar, + Varchar>({prefix + "str_to_map"}); + registerFunction( {prefix + "left"}); diff --git a/velox/functions/sparksql/StringToMap.h b/velox/functions/sparksql/StringToMap.h new file mode 100644 index 000000000000..cc159b5752dd --- /dev/null +++ b/velox/functions/sparksql/StringToMap.h @@ -0,0 +1,103 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed 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. + */ +#pragma once + +#include "folly/container/F14Set.h" +#include "velox/functions/Udf.h" + +namespace facebook::velox::functions::sparksql { + +template +struct StringToMapFunction { + VELOX_DEFINE_FUNCTION_TYPES(TExecCtx); + + // Results refer to strings in the first argument. + static constexpr int32_t reuse_strings_from_arg = 0; + + void call( + out_type>& out, + const arg_type& input, + const arg_type& entryDelimiter, + const arg_type& keyValueDelimiter) { + VELOX_USER_CHECK(!entryDelimiter.empty(), "entryDelimiter is empty"); + VELOX_USER_CHECK(!keyValueDelimiter.empty(), "keyValueDelimiter is empty"); + + callImpl( + out, + toStringView(input), + toStringView(entryDelimiter), + toStringView(keyValueDelimiter)); + } + + private: + static std::string_view toStringView(const arg_type& input) { + return std::string_view(input.data(), input.size()); + } + + void callImpl( + out_type>& out, + std::string_view input, + std::string_view entryDelimiter, + std::string_view keyValueDelimiter) const { + size_t pos = 0; + + folly::F14FastSet keys; + + auto nextEntryPos = input.find(entryDelimiter, pos); + while (nextEntryPos != std::string::npos) { + processEntry( + out, + std::string_view(input.data() + pos, nextEntryPos - pos), + keyValueDelimiter, + keys); + + pos = nextEntryPos + 1; + nextEntryPos = input.find(entryDelimiter, pos); + } + + processEntry( + out, + std::string_view(input.data() + pos, input.size() - pos), + keyValueDelimiter, + keys); + } + + void processEntry( + out_type>& out, + std::string_view entry, + std::string_view keyValueDelimiter, + folly::F14FastSet& keys) const { + const auto delimiterPos = entry.find(keyValueDelimiter, 0); + // Not found key/value delimiter. + if (delimiterPos == std::string::npos) { + out.add_null().setNoCopy(StringView(entry)); + return; + } + const auto key = std::string_view(entry.data(), delimiterPos); + VELOX_USER_CHECK( + keys.insert(key).second, + "Duplicate keys are not allowed: ('{}').", + key); + const auto value = StringView( + entry.data() + delimiterPos + 1, entry.size() - delimiterPos - 1); + + auto [keyWriter, valueWriter] = out.add_item(); + keyWriter.setNoCopy(StringView(key)); + valueWriter.setNoCopy(value); + } +}; + +} // namespace facebook::velox::functions::sparksql diff --git a/velox/functions/sparksql/tests/CMakeLists.txt b/velox/functions/sparksql/tests/CMakeLists.txt index 7e7650e12173..49b63c10d815 100644 --- a/velox/functions/sparksql/tests/CMakeLists.txt +++ b/velox/functions/sparksql/tests/CMakeLists.txt @@ -38,6 +38,7 @@ add_executable( SortArrayTest.cpp SplitFunctionsTest.cpp StringTest.cpp + StringToMapTest.cpp UnscaledValueFunctionTest.cpp XxHash64Test.cpp) diff --git a/velox/functions/sparksql/tests/StringToMapTest.cpp b/velox/functions/sparksql/tests/StringToMapTest.cpp new file mode 100644 index 000000000000..571aa6d2faa2 --- /dev/null +++ b/velox/functions/sparksql/tests/StringToMapTest.cpp @@ -0,0 +1,66 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed 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 "velox/common/base/tests/GTestUtils.h" +#include "velox/functions/sparksql/tests/SparkFunctionBaseTest.h" +#include "velox/type/Type.h" + +namespace facebook::velox::functions::sparksql::test { +using namespace facebook::velox::test; +namespace { +class StringToMapTest : public SparkFunctionBaseTest { + protected: + void testStringToMap( + const std::vector& inputs, + const std::vector>>& + expect) { + std::vector row; + row.emplace_back(makeFlatVector({inputs[0]})); + std::string expr = + fmt::format("str_to_map(c0, '{}', '{}')", inputs[1], inputs[2]); + auto result = evaluate(expr, makeRowVector(row)); + auto expected = makeMapVector({expect}); + assertEqualVectors(result, expected); + } +}; + +TEST_F(StringToMapTest, Basics) { + testStringToMap( + {"a:1,b:2,c:3", ",", ":"}, {{"a", "1"}, {"b", "2"}, {"c", "3"}}); + testStringToMap({"a: ,b:2", ",", ":"}, {{"a", " "}, {"b", "2"}}); + testStringToMap({"", ",", ":"}, {{"", std::nullopt}}); + testStringToMap({"a", ",", ":"}, {{"a", std::nullopt}}); + testStringToMap( + {"a=1,b=2,c=3", ",", "="}, {{"a", "1"}, {"b", "2"}, {"c", "3"}}); + testStringToMap({"", ",", "="}, {{"", std::nullopt}}); + testStringToMap( + {"a::1,b::2,c::3", ",", "c"}, + {{"", "::3"}, {"a::1", std::nullopt}, {"b::2", std::nullopt}}); + testStringToMap( + {"a:1_b:2_c:3", "_", ":"}, {{"a", "1"}, {"b", "2"}, {"c", "3"}}); + // Same delimiters. + testStringToMap( + {"a:1,b:2,c:3", ",", ","}, + {{"a:1", std::nullopt}, {"b:2", std::nullopt}, {"c:3", std::nullopt}}); + testStringToMap( + {"a:1_b:2_c:3", "_", "_"}, + {{"a:1", std::nullopt}, {"b:2", std::nullopt}, {"c:3", std::nullopt}}); + // Exception for duplicated keys. + VELOX_ASSERT_THROW( + testStringToMap({"a:1,b:2,a:3", ",", ":"}, {{"a", "3"}, {"b", "2"}}), + "Duplicate keys are not allowed: ('a')."); +} +} // namespace +} // namespace facebook::velox::functions::sparksql::test From e0487e33975a83eea37fb84c4c13cddd1a0cf981 Mon Sep 17 00:00:00 2001 From: PHILO-HE Date: Wed, 6 Dec 2023 11:17:50 +0800 Subject: [PATCH 2/4] Fix review comments --- velox/docs/functions/spark/string.rst | 4 +-- velox/functions/sparksql/StringToMap.h | 11 +++---- .../sparksql/tests/StringToMapTest.cpp | 31 ++++++++++++------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/velox/docs/functions/spark/string.rst b/velox/docs/functions/spark/string.rst index 8c3d9d2f9d99..32ebd360d824 100644 --- a/velox/docs/functions/spark/string.rst +++ b/velox/docs/functions/spark/string.rst @@ -187,8 +187,8 @@ Unless specified otherwise, all functions return NULL if at least one of the arg Spark's default behavior. :: SELECT str_to_map('a:1,b:2,c:3', ',', ':'); -- {"a":"1","b":"2","c":"3"} - SELECT str_to_map('a', ',', ':'); -- {'a':NULL} - SELECT str_to_map('', ',', ':'); -- {'':NULL} + SELECT str_to_map('a', ',', ':'); -- {"a":NULL} + SELECT str_to_map('', ',', ':'); -- {"":NULL} SELECT str_to_map('a:1,b:2,c:3', ',', ','); -- {"a:1":NULL,"b:2":NULL,"c:3":NULL} .. spark:function:: substring(string, start) -> varchar diff --git a/velox/functions/sparksql/StringToMap.h b/velox/functions/sparksql/StringToMap.h index cc159b5752dd..3f2525eff8da 100644 --- a/velox/functions/sparksql/StringToMap.h +++ b/velox/functions/sparksql/StringToMap.h @@ -20,9 +20,9 @@ namespace facebook::velox::functions::sparksql { -template +template struct StringToMapFunction { - VELOX_DEFINE_FUNCTION_TYPES(TExecCtx); + VELOX_DEFINE_FUNCTION_TYPES(T); // Results refer to strings in the first argument. static constexpr int32_t reuse_strings_from_arg = 0; @@ -32,8 +32,8 @@ struct StringToMapFunction { const arg_type& input, const arg_type& entryDelimiter, const arg_type& keyValueDelimiter) { - VELOX_USER_CHECK(!entryDelimiter.empty(), "entryDelimiter is empty"); - VELOX_USER_CHECK(!keyValueDelimiter.empty(), "keyValueDelimiter is empty"); + VELOX_USER_CHECK(!entryDelimiter.empty(), "entryDelimiter is empty."); + VELOX_USER_CHECK(!keyValueDelimiter.empty(), "keyValueDelimiter is empty."); callImpl( out, @@ -53,7 +53,6 @@ struct StringToMapFunction { std::string_view entryDelimiter, std::string_view keyValueDelimiter) const { size_t pos = 0; - folly::F14FastSet keys; auto nextEntryPos = input.find(entryDelimiter, pos); @@ -81,7 +80,7 @@ struct StringToMapFunction { std::string_view keyValueDelimiter, folly::F14FastSet& keys) const { const auto delimiterPos = entry.find(keyValueDelimiter, 0); - // Not found key/value delimiter. + // Allows keyValue delimiter not found. if (delimiterPos == std::string::npos) { out.add_null().setNoCopy(StringView(entry)); return; diff --git a/velox/functions/sparksql/tests/StringToMapTest.cpp b/velox/functions/sparksql/tests/StringToMapTest.cpp index 571aa6d2faa2..168725cc4eaf 100644 --- a/velox/functions/sparksql/tests/StringToMapTest.cpp +++ b/velox/functions/sparksql/tests/StringToMapTest.cpp @@ -15,24 +15,27 @@ */ #include "velox/common/base/tests/GTestUtils.h" #include "velox/functions/sparksql/tests/SparkFunctionBaseTest.h" -#include "velox/type/Type.h" -namespace facebook::velox::functions::sparksql::test { using namespace facebook::velox::test; + +namespace facebook::velox::functions::sparksql::test { namespace { class StringToMapTest : public SparkFunctionBaseTest { protected: + VectorPtr evaluateStringToMap(const std::vector& inputs) { + const std::string expr = + fmt::format("str_to_map(c0, '{}', '{}')", inputs[1], inputs[2]); + return evaluate( + expr, makeRowVector({makeFlatVector({inputs[0]})})); + } + void testStringToMap( const std::vector& inputs, const std::vector>>& expect) { - std::vector row; - row.emplace_back(makeFlatVector({inputs[0]})); - std::string expr = - fmt::format("str_to_map(c0, '{}', '{}')", inputs[1], inputs[2]); - auto result = evaluate(expr, makeRowVector(row)); - auto expected = makeMapVector({expect}); - assertEqualVectors(result, expected); + auto result = evaluateStringToMap(inputs); + auto expectVector = makeMapVector({expect}); + assertEqualVectors(result, expectVector); } }; @@ -40,6 +43,7 @@ TEST_F(StringToMapTest, Basics) { testStringToMap( {"a:1,b:2,c:3", ",", ":"}, {{"a", "1"}, {"b", "2"}, {"c", "3"}}); testStringToMap({"a: ,b:2", ",", ":"}, {{"a", " "}, {"b", "2"}}); + testStringToMap({"a:,b:2", ",", ":"}, {{"a", ""}, {"b", "2"}}); testStringToMap({"", ",", ":"}, {{"", std::nullopt}}); testStringToMap({"a", ",", ":"}, {{"a", std::nullopt}}); testStringToMap( @@ -47,9 +51,10 @@ TEST_F(StringToMapTest, Basics) { testStringToMap({"", ",", "="}, {{"", std::nullopt}}); testStringToMap( {"a::1,b::2,c::3", ",", "c"}, - {{"", "::3"}, {"a::1", std::nullopt}, {"b::2", std::nullopt}}); + {{"a::1", std::nullopt}, {"b::2", std::nullopt}, {"", "::3"}}); testStringToMap( {"a:1_b:2_c:3", "_", ":"}, {{"a", "1"}, {"b", "2"}, {"c", "3"}}); + // Same delimiters. testStringToMap( {"a:1,b:2,c:3", ",", ","}, @@ -57,10 +62,14 @@ TEST_F(StringToMapTest, Basics) { testStringToMap( {"a:1_b:2_c:3", "_", "_"}, {{"a:1", std::nullopt}, {"b:2", std::nullopt}, {"c:3", std::nullopt}}); + // Exception for duplicated keys. VELOX_ASSERT_THROW( - testStringToMap({"a:1,b:2,a:3", ",", ":"}, {{"a", "3"}, {"b", "2"}}), + evaluateStringToMap({"a:1,b:2,a:3", ",", ":"}), "Duplicate keys are not allowed: ('a')."); + VELOX_ASSERT_THROW( + evaluateStringToMap({":1,:2", ",", ":"}), + "Duplicate keys are not allowed: ('')."); } } // namespace } // namespace facebook::velox::functions::sparksql::test From b8aadc88b0530f7166afca4739a74b53344f9e1c Mon Sep 17 00:00:00 2001 From: PHILO-HE Date: Wed, 6 Dec 2023 16:07:06 +0800 Subject: [PATCH 3/4] Fix review comments --- velox/docs/functions/spark/string.rst | 15 ++++++++------- velox/functions/sparksql/StringToMap.h | 10 +++++----- .../functions/sparksql/tests/StringToMapTest.cpp | 4 ++-- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/velox/docs/functions/spark/string.rst b/velox/docs/functions/spark/string.rst index 32ebd360d824..5c59f4e00018 100644 --- a/velox/docs/functions/spark/string.rst +++ b/velox/docs/functions/spark/string.rst @@ -178,13 +178,14 @@ Unless specified otherwise, all functions return NULL if at least one of the arg SELECT startswith('js SQL', 'SQL'); -- false SELECT startswith('js SQL', null); -- NULL -.. spark:function:: str_to_map(string, entryDelim, keyValueDelim) -> map(string, string) - - Returns a map by splitting ``string`` into entries with ``entryDelim`` and splitting - each entry into key/value with ``keyValueDelim``. - Only supports constant single-character ``entryDelim`` and ``keyValueDelim``. Throws - exception when duplicate map keys are found for single row's result, consistent with - Spark's default behavior. :: +.. spark:function:: str_to_map(string, entryDelimiter, keyValueDelimiter) -> map(string, string) + + Returns a map by splitting ``string`` into entries with ``entryDelimiter`` and splitting + each entry into key/value with ``keyValueDelimiter``. + ``entryDelimiter`` and ``keyValueDelimiter`` must be constant strings with single ascii + character. Allows ``keyValueDelimiter`` not found when splitting an entry. Throws exception + when duplicate map keys are found for single row's result, consistent with Spark's default + behavior. :: SELECT str_to_map('a:1,b:2,c:3', ',', ':'); -- {"a":"1","b":"2","c":"3"} SELECT str_to_map('a', ',', ':'); -- {"a":NULL} diff --git a/velox/functions/sparksql/StringToMap.h b/velox/functions/sparksql/StringToMap.h index 3f2525eff8da..7bd6edee454e 100644 --- a/velox/functions/sparksql/StringToMap.h +++ b/velox/functions/sparksql/StringToMap.h @@ -32,8 +32,10 @@ struct StringToMapFunction { const arg_type& input, const arg_type& entryDelimiter, const arg_type& keyValueDelimiter) { - VELOX_USER_CHECK(!entryDelimiter.empty(), "entryDelimiter is empty."); - VELOX_USER_CHECK(!keyValueDelimiter.empty(), "keyValueDelimiter is empty."); + VELOX_USER_CHECK( + entryDelimiter.size() == 1, "entryDelimiter's size should be 1."); + VELOX_USER_CHECK( + keyValueDelimiter.size() == 1, "keyValueDelimiter's size should be 1."); callImpl( out, @@ -87,9 +89,7 @@ struct StringToMapFunction { } const auto key = std::string_view(entry.data(), delimiterPos); VELOX_USER_CHECK( - keys.insert(key).second, - "Duplicate keys are not allowed: ('{}').", - key); + keys.insert(key).second, "Duplicate keys are not allowed: '{}'.", key); const auto value = StringView( entry.data() + delimiterPos + 1, entry.size() - delimiterPos - 1); diff --git a/velox/functions/sparksql/tests/StringToMapTest.cpp b/velox/functions/sparksql/tests/StringToMapTest.cpp index 168725cc4eaf..bea1453c646d 100644 --- a/velox/functions/sparksql/tests/StringToMapTest.cpp +++ b/velox/functions/sparksql/tests/StringToMapTest.cpp @@ -66,10 +66,10 @@ TEST_F(StringToMapTest, Basics) { // Exception for duplicated keys. VELOX_ASSERT_THROW( evaluateStringToMap({"a:1,b:2,a:3", ",", ":"}), - "Duplicate keys are not allowed: ('a')."); + "Duplicate keys are not allowed: 'a'."); VELOX_ASSERT_THROW( evaluateStringToMap({":1,:2", ",", ":"}), - "Duplicate keys are not allowed: ('')."); + "Duplicate keys are not allowed: ''."); } } // namespace } // namespace facebook::velox::functions::sparksql::test From e3d4647c5f62cbb17dd6c47f7a6cc596fbd6f93d Mon Sep 17 00:00:00 2001 From: PHILO-HE Date: Wed, 6 Dec 2023 16:34:55 +0800 Subject: [PATCH 4/4] Add tests for exception cases --- velox/functions/sparksql/StringToMap.h | 8 +++--- .../sparksql/tests/StringToMapTest.cpp | 25 ++++++++++++++++++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/velox/functions/sparksql/StringToMap.h b/velox/functions/sparksql/StringToMap.h index 7bd6edee454e..3e635a86b172 100644 --- a/velox/functions/sparksql/StringToMap.h +++ b/velox/functions/sparksql/StringToMap.h @@ -32,10 +32,10 @@ struct StringToMapFunction { const arg_type& input, const arg_type& entryDelimiter, const arg_type& keyValueDelimiter) { - VELOX_USER_CHECK( - entryDelimiter.size() == 1, "entryDelimiter's size should be 1."); - VELOX_USER_CHECK( - keyValueDelimiter.size() == 1, "keyValueDelimiter's size should be 1."); + VELOX_USER_CHECK_EQ( + entryDelimiter.size(), 1, "entryDelimiter's size should be 1."); + VELOX_USER_CHECK_EQ( + keyValueDelimiter.size(), 1, "keyValueDelimiter's size should be 1."); callImpl( out, diff --git a/velox/functions/sparksql/tests/StringToMapTest.cpp b/velox/functions/sparksql/tests/StringToMapTest.cpp index bea1453c646d..3cfbb24ef8da 100644 --- a/velox/functions/sparksql/tests/StringToMapTest.cpp +++ b/velox/functions/sparksql/tests/StringToMapTest.cpp @@ -39,7 +39,7 @@ class StringToMapTest : public SparkFunctionBaseTest { } }; -TEST_F(StringToMapTest, Basics) { +TEST_F(StringToMapTest, basic) { testStringToMap( {"a:1,b:2,c:3", ",", ":"}, {{"a", "1"}, {"b", "2"}, {"c", "3"}}); testStringToMap({"a: ,b:2", ",", ":"}, {{"a", " "}, {"b", "2"}}); @@ -63,6 +63,29 @@ TEST_F(StringToMapTest, Basics) { {"a:1_b:2_c:3", "_", "_"}, {{"a:1", std::nullopt}, {"b:2", std::nullopt}, {"c:3", std::nullopt}}); + // Exception for illegal delimiters. + // Empty string is used. + VELOX_ASSERT_THROW( + evaluateStringToMap({"a:1,b:2", "", ":"}), + "entryDelimiter's size should be 1."); + VELOX_ASSERT_THROW( + evaluateStringToMap({"a:1,b:2", ",", ""}), + "keyValueDelimiter's size should be 1."); + // Delimiter's length > 1. + VELOX_ASSERT_THROW( + evaluateStringToMap({"a:1,b:2", ";;", ":"}), + "entryDelimiter's size should be 1."); + VELOX_ASSERT_THROW( + evaluateStringToMap({"a:1,b:2", ",", "::"}), + "keyValueDelimiter's size should be 1."); + // Unicode character is used. + VELOX_ASSERT_THROW( + evaluateStringToMap({"a:1,b:2", "å", ":"}), + "entryDelimiter's size should be 1."); + VELOX_ASSERT_THROW( + evaluateStringToMap({"a:1,b:2", ",", "æ"}), + "keyValueDelimiter's size should be 1."); + // Exception for duplicated keys. VELOX_ASSERT_THROW( evaluateStringToMap({"a:1,b:2,a:3", ",", ":"}),