From 58050116290a57ca3bbe2d5e7158826c49af4afb Mon Sep 17 00:00:00 2001 From: Jerry Hu Date: Fri, 28 Oct 2022 10:43:56 +0800 Subject: [PATCH] [Feature](string-function) Add function mask/mask_first_n/mask_last_n (#13694) Implementation of mask function from hive. --- be/src/vec/functions/function_string.cpp | 3 + be/src/vec/functions/function_string.h | 177 ++++++++++++++++++ .../string-functions/mask/mask.md | 66 +++++++ .../string-functions/mask/mask_first_n.md | 57 ++++++ .../string-functions/mask/mask_last_n.md | 57 ++++++ docs/sidebars.json | 3 + .../string-functions/mask/mask.md | 66 +++++++ .../string-functions/mask/mask_first_n.md | 57 ++++++ .../string-functions/mask/mask_last_n.md | 57 ++++++ gensrc/script/doris_builtins_functions.py | 5 + .../correctness_p0/test_mask_function.out | 57 ++++++ .../correctness_p0/test_mask_function.groovy | 74 ++++++++ 12 files changed, 679 insertions(+) create mode 100644 docs/en/docs/sql-manual/sql-functions/string-functions/mask/mask.md create mode 100644 docs/en/docs/sql-manual/sql-functions/string-functions/mask/mask_first_n.md create mode 100644 docs/en/docs/sql-manual/sql-functions/string-functions/mask/mask_last_n.md create mode 100644 docs/zh-CN/docs/sql-manual/sql-functions/string-functions/mask/mask.md create mode 100644 docs/zh-CN/docs/sql-manual/sql-functions/string-functions/mask/mask_first_n.md create mode 100644 docs/zh-CN/docs/sql-manual/sql-functions/string-functions/mask/mask_last_n.md create mode 100644 regression-test/data/correctness_p0/test_mask_function.out create mode 100644 regression-test/suites/correctness_p0/test_mask_function.groovy diff --git a/be/src/vec/functions/function_string.cpp b/be/src/vec/functions/function_string.cpp index f694d274ce..1265a0e0b4 100644 --- a/be/src/vec/functions/function_string.cpp +++ b/be/src/vec/functions/function_string.cpp @@ -686,6 +686,9 @@ void register_function_string(SimpleFunctionFactory& factory) { factory.register_function>(); factory.register_function>(); factory.register_function(); + factory.register_function(); + factory.register_function>(); + factory.register_function>(); factory.register_function>(); factory.register_function>(); diff --git a/be/src/vec/functions/function_string.h b/be/src/vec/functions/function_string.h index c652f8deef..5aac7a1572 100644 --- a/be/src/vec/functions/function_string.h +++ b/be/src/vec/functions/function_string.h @@ -288,6 +288,183 @@ struct Substr2Impl { } }; +template +class FunctionMaskPartial; +class FunctionMask : public IFunction { +public: + static constexpr auto name = "mask"; + static constexpr unsigned char DEFAULT_UPPER_MASK = 'X'; + static constexpr unsigned char DEFAULT_LOWER_MASK = 'x'; + static constexpr unsigned char DEFAULT_NUMBER_MASK = 'n'; + String get_name() const override { return name; } + static FunctionPtr create() { return std::make_shared(); } + + DataTypePtr get_return_type_impl(const DataTypes& arguments) const override { + return std::make_shared(); + } + + size_t get_number_of_arguments() const override { return 0; } + + bool is_variadic() 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 { + DCHECK_GE(arguments.size(), 1); + DCHECK_LE(arguments.size(), 4); + + char upper = DEFAULT_UPPER_MASK, lower = DEFAULT_LOWER_MASK, number = DEFAULT_NUMBER_MASK; + + auto res = ColumnString::create(); + const auto& source_column = + assert_cast(*block.get_by_position(arguments[0]).column); + + if (arguments.size() > 1) { + auto& col = *block.get_by_position(arguments[1]).column; + auto string_ref = col.get_data_at(0); + if (string_ref.size > 0) upper = *string_ref.data; + } + + if (arguments.size() > 2) { + auto& col = *block.get_by_position(arguments[2]).column; + auto string_ref = col.get_data_at(0); + if (string_ref.size > 0) lower = *string_ref.data; + } + + if (arguments.size() > 3) { + auto& col = *block.get_by_position(arguments[3]).column; + auto string_ref = col.get_data_at(0); + if (string_ref.size > 0) number = *string_ref.data; + } + + if (arguments.size() > 4) { + return Status::InvalidArgument( + fmt::format("too many arguments for function {}", get_name())); + } + + vector_mask(source_column, *res, upper, lower, number); + + block.get_by_position(result).column = std::move(res); + + return Status::OK(); + } + friend class FunctionMaskPartial; + friend class FunctionMaskPartial; + +private: + static void vector_mask(const ColumnString& source, ColumnString& result, const char upper, + const char lower, const char number) { + result.get_chars().resize(source.get_chars().size()); + result.get_offsets().resize(source.get_offsets().size()); + memcpy(result.get_offsets().data(), source.get_offsets().data(), + source.get_offsets().size() * sizeof(ColumnString::Offset)); + + const unsigned char* src = source.get_chars().data(); + const size_t size = source.get_chars().size(); + unsigned char* res = result.get_chars().data(); + mask(src, size, upper, lower, number, res); + } + + static void mask(const unsigned char* __restrict src, const size_t size, + const unsigned char upper, const unsigned char lower, + const unsigned char number, unsigned char* __restrict res) { + for (size_t i = 0; i != size; ++i) { + auto c = src[i]; + if (c >= 'A' && c <= 'Z') { + res[i] = upper; + } else if (c >= 'a' && c <= 'z') { + res[i] = lower; + } else if (c >= '0' && c <= '9') { + res[i] = number; + } else { + res[i] = c; + } + } + } +}; + +template +class FunctionMaskPartial : public IFunction { +public: + static constexpr auto name = Reverse ? "mask_last_n" : "mask_first_n"; + String get_name() const override { return name; } + static FunctionPtr create() { return std::make_shared(); } + + DataTypePtr get_return_type_impl(const DataTypes& arguments) const override { + return std::make_shared(); + } + + size_t get_number_of_arguments() const override { return 0; } + + bool is_variadic() 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 { + DCHECK_GE(arguments.size(), 1); + DCHECK_LE(arguments.size(), 2); + + int n = -1; + + auto res = ColumnString::create(); + const ColumnString& source_column = + assert_cast(*block.get_by_position(arguments[0]).column); + + if (arguments.size() == 2) { + auto& col = *block.get_by_position(arguments[1]).column; + n = col.get_int(0); + } else if (arguments.size() > 2) { + return Status::InvalidArgument( + fmt::format("too many arguments for function {}", get_name())); + } + + if (n == -1) { + FunctionMask::vector_mask(source_column, *res, FunctionMask::DEFAULT_UPPER_MASK, + FunctionMask::DEFAULT_LOWER_MASK, + FunctionMask::DEFAULT_NUMBER_MASK); + } else if (n > 0) { + vector(source_column, n, *res); + } + + block.get_by_position(result).column = std::move(res); + + return Status::OK(); + } + +private: + static void vector(const ColumnString& src, int n, ColumnString& result) { + const auto num_rows = src.size(); + auto* chars = src.get_chars().data(); + auto* offsets = src.get_offsets().data(); + result.get_chars().resize(src.get_chars().size()); + result.get_offsets().resize(src.get_offsets().size()); + memcpy(result.get_offsets().data(), src.get_offsets().data(), + src.get_offsets().size() * sizeof(ColumnString::Offset)); + auto* res = result.get_chars().data(); + + for (ssize_t i = 0; i != num_rows; ++i) { + auto offset = offsets[i - 1]; + int len = offsets[i] - offset; + if constexpr (Reverse) { + auto start = std::max(len - n, 0); + if (start > 0) memcpy(&res[offset], &chars[offset], start); + offset += start; + } else { + if (n < len) memcpy(&res[offset + n], &chars[offset + n], len - n); + } + + len = std::min(n, len); + FunctionMask::mask(&chars[offset], len, FunctionMask::DEFAULT_UPPER_MASK, + FunctionMask::DEFAULT_LOWER_MASK, FunctionMask::DEFAULT_NUMBER_MASK, + &res[offset]); + } + } +}; + class FunctionLeft : public IFunction { public: static constexpr auto name = "left"; diff --git a/docs/en/docs/sql-manual/sql-functions/string-functions/mask/mask.md b/docs/en/docs/sql-manual/sql-functions/string-functions/mask/mask.md new file mode 100644 index 0000000000..f758a8b946 --- /dev/null +++ b/docs/en/docs/sql-manual/sql-functions/string-functions/mask/mask.md @@ -0,0 +1,66 @@ +--- +{ + "title": "mask", + "language": "en" +} +--- + + + +## mask +### description +#### syntax + +`VARCHAR mask(VARCHAR str, [, VARCHAR upper[, VARCHAR lower[, VARCHAR number]]])` + +Returns a masked version of str . By default, upper case letters are converted to "X", lower case letters are converted to "x" and numbers are converted to "n". For example mask("abcd-EFGH-8765-4321") results in xxxx-XXXX-nnnn-nnnn. You can override the characters used in the mask by supplying additional arguments: the second argument controls the mask character for upper case letters, the third argument for lower case letters and the fourth argument for numbers. For example, mask("abcd-EFGH-8765-4321", "U", "l", "#") results in llll-UUUU-####-####. + +### example + +``` +// table test ++-----------+ +| name | ++-----------+ +| abc123EFG | +| NULL | +| 456AbCdEf | ++-----------+ + +mysql> select mask(name) from test; ++--------------+ +| mask(`name`) | ++--------------+ +| xxxnnnXXX | +| NULL | +| nnnXxXxXx | ++--------------+ + +mysql> select mask(name, '*', '#', '$') from test; ++-----------------------------+ +| mask(`name`, '*', '#', '$') | ++-----------------------------+ +| ###$$$*** | +| NULL | +| $$$*#*#*# | ++-----------------------------+ + +### keywords + mask diff --git a/docs/en/docs/sql-manual/sql-functions/string-functions/mask/mask_first_n.md b/docs/en/docs/sql-manual/sql-functions/string-functions/mask/mask_first_n.md new file mode 100644 index 0000000000..d1691c20f0 --- /dev/null +++ b/docs/en/docs/sql-manual/sql-functions/string-functions/mask/mask_first_n.md @@ -0,0 +1,57 @@ +--- +{ + "title": "mask_first_n", + "language": "en" +} +--- + + + +## mask_first_n +### description +#### syntax + +`VARCHAR mask_first_n(VARCHAR str, [, INT n])` + +Returns a masked version of str with the first n values masked. Upper case letters are converted to "X", lower case letters are converted to "x" and numbers are converted to "n". For example, mask_first_n("1234-5678-8765-4321", 4) results in nnnn-5678-8765-4321. + +### example + +``` +// table test ++-----------+ +| name | ++-----------+ +| abc123EFG | +| NULL | +| 456AbCdEf | ++-----------+ + +mysql> select mask_first_n(name, 5) from test; ++-------------------------+ +| mask_first_n(`name`, 5) | ++-------------------------+ +| xxxnn3EFG | +| NULL | +| nnnXxCdEf | ++-------------------------+ + +### keywords + mask_first_n diff --git a/docs/en/docs/sql-manual/sql-functions/string-functions/mask/mask_last_n.md b/docs/en/docs/sql-manual/sql-functions/string-functions/mask/mask_last_n.md new file mode 100644 index 0000000000..794722a8da --- /dev/null +++ b/docs/en/docs/sql-manual/sql-functions/string-functions/mask/mask_last_n.md @@ -0,0 +1,57 @@ +--- +{ + "title": "mask_last_n", + "language": "en" +} +--- + + + +## mask_last_n +### description +#### syntax + +`VARCHAR mask_last_n(VARCHAR str, [, INT n])` + +Returns a masked version of str with the last n values masked. Upper case letters are converted to "X", lower case letters are converted to "x" and numbers are converted to "n". For example, mask_last_n("1234-5678-8765-4321", 4) results in 1234-5678-8765-nnnn. + +### example + +``` +// table test ++-----------+ +| name | ++-----------+ +| abc123EFG | +| NULL | +| 456AbCdEf | ++-----------+ + +mysql> select mask_last_n(name, 5) from test; ++------------------------+ +| mask_last_n(`name`, 5) | ++------------------------+ +| abc1nnXXX | +| NULL | +| 456AxXxXx | ++------------------------+ + +### keywords + mask_last_n diff --git a/docs/sidebars.json b/docs/sidebars.json index 6d515f636d..0333af1eeb 100644 --- a/docs/sidebars.json +++ b/docs/sidebars.json @@ -396,6 +396,9 @@ "sql-manual/sql-functions/string-functions/money_format", "sql-manual/sql-functions/string-functions/parse_url", "sql-manual/sql-functions/string-functions/extract_url_parameter", + "sql-manual/sql-functions/string-functions/mask/mask", + "sql-manual/sql-functions/string-functions/mask/mask_first_n", + "sql-manual/sql-functions/string-functions/mask/mask_last_n", { "type": "category", "label": "Fuzzy Match", diff --git a/docs/zh-CN/docs/sql-manual/sql-functions/string-functions/mask/mask.md b/docs/zh-CN/docs/sql-manual/sql-functions/string-functions/mask/mask.md new file mode 100644 index 0000000000..576c65e69d --- /dev/null +++ b/docs/zh-CN/docs/sql-manual/sql-functions/string-functions/mask/mask.md @@ -0,0 +1,66 @@ +--- +{ + "title": "mask", + "language": "zh-CN" +} +--- + + + +## mask +### description +#### syntax + +`VARCHAR mask(VARCHAR str, [, VARCHAR upper[, VARCHAR lower[, VARCHAR number]]])` + +返回 str 的掩码版本。 默认情况下,大写字母转换为“X”,小写字母转换为“x”,数字转换为“n”。 例如 mask("abcd-EFGH-8765-4321") 结果为 xxxx-XXXX-nnnn-nnnn。 您可以通过提供附加参数来覆盖掩码中使用的字符:第二个参数控制大写字母的掩码字符,第三个参数控制小写字母,第四个参数控制数字。 例如,mask("abcd-EFGH-8765-4321", "U", "l", "#") 会得到 llll-UUUU-####-####。 + +### example + +``` +// table test ++-----------+ +| name | ++-----------+ +| abc123EFG | +| NULL | +| 456AbCdEf | ++-----------+ + +mysql> select mask(name) from test; ++--------------+ +| mask(`name`) | ++--------------+ +| xxxnnnXXX | +| NULL | +| nnnXxXxXx | ++--------------+ + +mysql> select mask(name, '*', '#', '$') from test; ++-----------------------------+ +| mask(`name`, '*', '#', '$') | ++-----------------------------+ +| ###$$$*** | +| NULL | +| $$$*#*#*# | ++-----------------------------+ + +### keywords + mask diff --git a/docs/zh-CN/docs/sql-manual/sql-functions/string-functions/mask/mask_first_n.md b/docs/zh-CN/docs/sql-manual/sql-functions/string-functions/mask/mask_first_n.md new file mode 100644 index 0000000000..221cb1adc7 --- /dev/null +++ b/docs/zh-CN/docs/sql-manual/sql-functions/string-functions/mask/mask_first_n.md @@ -0,0 +1,57 @@ +--- +{ + "title": "mask_first_n", + "language": "zh-CN" +} +--- + + + +## mask_first_n +### description +#### syntax + +`VARCHAR mask_first_n(VARCHAR str, [, INT n])` + +返回带有掩码的前 n 个值的 str 的掩码版本。 大写字母转换为“X”,小写字母转换为“x”,数字转换为“n”。 例如,mask_first_n("1234-5678-8765-4321", 4) 结果为 nnnn-5678-8765-4321。 + +### example + +``` +// table test ++-----------+ +| name | ++-----------+ +| abc123EFG | +| NULL | +| 456AbCdEf | ++-----------+ + +mysql> select mask_first_n(name, 5) from test; ++-------------------------+ +| mask_first_n(`name`, 5) | ++-------------------------+ +| xxxnn3EFG | +| NULL | +| nnnXxCdEf | ++-------------------------+ + +### keywords + mask_first_n diff --git a/docs/zh-CN/docs/sql-manual/sql-functions/string-functions/mask/mask_last_n.md b/docs/zh-CN/docs/sql-manual/sql-functions/string-functions/mask/mask_last_n.md new file mode 100644 index 0000000000..83a7979e79 --- /dev/null +++ b/docs/zh-CN/docs/sql-manual/sql-functions/string-functions/mask/mask_last_n.md @@ -0,0 +1,57 @@ +--- +{ + "title": "mask_last_n", + "language": "zh-CN" +} +--- + + + +## mask_last_n +### description +#### syntax + +`VARCHAR mask_last_n(VARCHAR str, [, INT n])` + +返回 str 的掩码版本,其中最后 n 个字符被转换为掩码。 大写字母转换为“X”,小写字母转换为“x”,数字转换为“n”。 例如,mask_last_n("1234-5678-8765-4321", 4) 结果为 1234-5678-8765-nnnn。 + +### example + +``` +// table test ++-----------+ +| name | ++-----------+ +| abc123EFG | +| NULL | +| 456AbCdEf | ++-----------+ + +mysql> select mask_last_n(name, 5) from test; ++------------------------+ +| mask_last_n(`name`, 5) | ++------------------------+ +| abc1nnXXX | +| NULL | +| 456AxXxXx | ++------------------------+ + +### keywords + mask_last_n diff --git a/gensrc/script/doris_builtins_functions.py b/gensrc/script/doris_builtins_functions.py index 444cf48716..caa609da3b 100755 --- a/gensrc/script/doris_builtins_functions.py +++ b/gensrc/script/doris_builtins_functions.py @@ -2065,6 +2065,11 @@ visible_functions = [ [['substr', 'substring'], 'VARCHAR', ['VARCHAR', 'INT', 'INT'], '_ZN5doris15StringFunctions9substringEPN' '9doris_udf15FunctionContextERKNS1_9StringValERKNS1_6IntValES9_', '', '', 'vec', 'ALWAYS_NULLABLE'], + [['mask'], 'STRING', ['STRING', '...'], '', '', '', 'vec', ''], + [['mask_first_n'], 'STRING', ['STRING'], '', '', '', 'vec', ''], + [['mask_first_n'], 'STRING', ['STRING', 'INT'], '', '', '', 'vec', ''], + [['mask_last_n'], 'STRING', ['STRING'], '', '', '', 'vec', ''], + [['mask_last_n'], 'STRING', ['STRING', 'INT'], '', '', '', 'vec', ''], [['strleft', 'left'], 'VARCHAR', ['VARCHAR', 'INT'], '_ZN5doris15StringFunctions4leftEPN9doris_udf' '15FunctionContextERKNS1_9StringValERKNS1_6IntValE', '', '', 'vec', 'ALWAYS_NULLABLE'], diff --git a/regression-test/data/correctness_p0/test_mask_function.out b/regression-test/data/correctness_p0/test_mask_function.out new file mode 100644 index 0000000000..a0b1d15e7f --- /dev/null +++ b/regression-test/data/correctness_p0/test_mask_function.out @@ -0,0 +1,57 @@ +-- This file is automatically generated. You should know what you did if you want to edit this +-- !select_all -- +1 Jack Hawk 18112349876 +2 Sam Smith \N +3 Tom Cruise 18212349876 +4 Bruce Willis \N +5 Ming Li 19943216789 +6 Meimei Han 13556780000 + +-- !select_mask -- +Jack Hawk Xxxx Xxxx *xxx *xxx *### *### +Sam Smith Xxx Xxxxx *xx *xxxx *## *#### +Tom Cruise Xxx Xxxxxx *xx *xxxxx *## *##### +Bruce Willis Xxxxx Xxxxxx *xxxx *xxxxx *#### *##### +Ming Li Xxxx Xx *xxx *x *### *# +Meimei Han Xxxxxx Xxx *xxxxx *xx *##### *## + +-- !select_mask_nullable -- +18112349876 nnnnnnnnnnn nnnnnnnnnnn @@@@@@@@@@@ +\N \N \N \N +18212349876 nnnnnnnnnnn nnnnnnnnnnn @@@@@@@@@@@ +\N \N \N \N +19943216789 nnnnnnnnnnn nnnnnnnnnnn @@@@@@@@@@@ +13556780000 nnnnnnnnnnn nnnnnnnnnnn @@@@@@@@@@@ + +-- !select_mask_first_n -- +Jack Hawk Xxxx Xxxx Xxxk Hawk Xxxx Xxxx +Sam Smith Xxx Xxxxx Xxx Smith Xxx Xxxxx +Tom Cruise Xxx Xxxxxx Xxx Cruise Xxx Xxxxxx +Bruce Willis Xxxxx Xxxxxx Xxxce Willis Xxxxx Xxxxxx +Ming Li Xxxx Xx Xxxg Li Xxxx Xx +Meimei Han Xxxxxx Xxx Xxxmei Han Xxxxxx Xxx + +-- !select_mask_first_n_nullable -- +18112349876 nnnnnnnnnnn nnn12349876 nnnnnnnnnnn +\N \N \N \N +18212349876 nnnnnnnnnnn nnn12349876 nnnnnnnnnnn +\N \N \N \N +19943216789 nnnnnnnnnnn nnn43216789 nnnnnnnnnnn +13556780000 nnnnnnnnnnn nnn56780000 nnnnnnnnnnn + +-- !select_mask_last_n -- +Jack Hawk Xxxx Xxxx Jack Hxxx Xxxx Xxxx +Sam Smith Xxx Xxxxx Sam Smxxx Xxx Xxxxx +Tom Cruise Xxx Xxxxxx Tom Cruxxx Xxx Xxxxxx +Bruce Willis Xxxxx Xxxxxx Bruce Wilxxx Xxxxx Xxxxxx +Ming Li Xxxx Xx Ming Xx Xxxx Xx +Meimei Han Xxxxxx Xxx Meimei Xxx Xxxxxx Xxx + +-- !select_mask_last_n_nullable -- +18112349876 nnnnnnnnnnn 18112349nnn nnnnnnnnnnn +\N \N \N \N +18212349876 nnnnnnnnnnn 18212349nnn nnnnnnnnnnn +\N \N \N \N +19943216789 nnnnnnnnnnn 19943216nnn nnnnnnnnnnn +13556780000 nnnnnnnnnnn 13556780nnn nnnnnnnnnnn + diff --git a/regression-test/suites/correctness_p0/test_mask_function.groovy b/regression-test/suites/correctness_p0/test_mask_function.groovy new file mode 100644 index 0000000000..0a582c2b43 --- /dev/null +++ b/regression-test/suites/correctness_p0/test_mask_function.groovy @@ -0,0 +1,74 @@ +// 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. + +suite("test_mask_function") { + sql """ + drop table if exists table_mask_test; + """ + + sql """ + create table table_mask_test ( + id int not null, + name varchar(20) not null, + phone char(11) null + ) + ENGINE=OLAP + distributed by hash(id) + properties( + 'replication_num' = '1' + ); + """ + + sql """ + insert into table_mask_test values + (1, 'Jack Hawk', '18112349876'), + (2, 'Sam Smith', null), + (3, 'Tom Cruise', '18212349876'), + (4, 'Bruce Willis', null), + (5, 'Ming Li', '19943216789'), + (6, 'Meimei Han', '13556780000') + ; + """ + + qt_select_all """ + select * from table_mask_test order by id; + """ + + qt_select_mask """ + select name, mask(name), mask(name, '*'), mask(name, '*', '#') from table_mask_test order by id; + """ + + qt_select_mask_nullable """ + select phone, mask(phone), mask(phone, '*'), mask(phone, '*', '#', '@') from table_mask_test order by id; + """ + + qt_select_mask_first_n """ + select name, mask_first_n(name), mask_first_n(name, 3), mask_first_n(name, 100) from table_mask_test order by id; + """ + + qt_select_mask_first_n_nullable """ + select phone, mask_first_n(phone), mask_first_n(phone, 3), mask_first_n(phone, 100) from table_mask_test order by id; + """ + + qt_select_mask_last_n """ + select name, mask_last_n(name), mask_last_n(name, 3), mask_last_n(name, 100) from table_mask_test order by id; + """ + + qt_select_mask_last_n_nullable """ + select phone, mask_last_n(phone), mask_last_n(phone, 3), mask_last_n(phone, 100) from table_mask_test order by id; + """ +}