summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--yql/essentials/core/type_ann/type_ann_core.cpp44
-rw-r--r--yql/essentials/data/language/sql_functions.json4
-rw-r--r--yql/essentials/docs/en/builtins/basic.md20
-rw-r--r--yql/essentials/docs/en/changelog/2025.04.md1
-rw-r--r--yql/essentials/docs/en/syntax/expressions.md2
-rw-r--r--yql/essentials/docs/ru/builtins/basic.md20
-rw-r--r--yql/essentials/docs/ru/changelog/2025.04.md1
-rw-r--r--yql/essentials/docs/ru/syntax/expressions.md2
-rw-r--r--yql/essentials/minikql/mkql_program_builder.cpp18
-rw-r--r--yql/essentials/minikql/mkql_program_builder.h1
-rw-r--r--yql/essentials/providers/common/mkql/yql_provider_mkql.cpp9
-rw-r--r--yql/essentials/sql/v1/builtin.cpp1
-rw-r--r--yql/essentials/tests/sql/minirun/part1/canondata/result.json14
-rw-r--r--yql/essentials/tests/sql/sql2yql/canondata/result.json12
-rw-r--r--yql/essentials/tests/sql/sql2yql/canondata/test_sql_format.test_expr-concat_many_/formatted.sql8
-rw-r--r--yql/essentials/tests/sql/suites/expr/concat_many.cfg2
-rw-r--r--yql/essentials/tests/sql/suites/expr/concat_many.yql5
17 files changed, 164 insertions, 0 deletions
diff --git a/yql/essentials/core/type_ann/type_ann_core.cpp b/yql/essentials/core/type_ann/type_ann_core.cpp
index 8320c08b502..2a3147c21a6 100644
--- a/yql/essentials/core/type_ann/type_ann_core.cpp
+++ b/yql/essentials/core/type_ann/type_ann_core.cpp
@@ -3298,6 +3298,49 @@ namespace NTypeAnnImpl {
return IGraphTransformer::TStatus::Ok;
}
+ IGraphTransformer::TStatus SqlConcatWrapper(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExtContext& ctx) {
+ if (!IsBackwardCompatibleFeatureAvailable(ctx.Types.LangVer, MakeLangVersion(2025, 04), ctx.Types.BackportMode)) {
+ ctx.Expr.AddError(TIssue(ctx.Expr.GetPosition(input->Pos()), "Concat function is not available before version 2025.04"));
+ return IGraphTransformer::TStatus::Error;
+ }
+
+ if (!EnsureMinArgsCount(*input, 1, ctx.Expr)) {
+ return IGraphTransformer::TStatus::Error;
+ }
+
+ bool hasOptionals = false;
+ bool hasBinary = false;
+ for (ui32 i = 0; i < input->ChildrenSize(); ++i) {
+ if (IsNull(*input->Child(i))) {
+ output = input->ChildPtr(i);
+ return IGraphTransformer::TStatus::Repeat;
+ }
+
+ bool isOptional;
+ const TDataExprType* dataType ;
+ if (!EnsureDataOrOptionalOfData(*input->Child(i), isOptional, dataType, ctx.Expr)) {
+ return IGraphTransformer::TStatus::Error;
+ }
+
+ if (dataType->GetSlot() != EDataSlot::String && dataType->GetSlot() != EDataSlot::Utf8) {
+ ctx.Expr.AddError(TIssue(ctx.Expr.GetPosition(input->Child(i)->Pos()),
+ TStringBuilder() << "Expected (optional) String or Utf8, but got: " << *input->Child(i)->GetTypeAnn()));
+ return IGraphTransformer::TStatus::Error;
+ }
+
+ hasOptionals = hasOptionals || isOptional;
+ hasBinary = hasBinary || (dataType->GetSlot() == EDataSlot::String);
+ }
+
+ const TTypeAnnotationNode* retType = ctx.Expr.MakeType<TDataExprType>(hasBinary ? EDataSlot::String : EDataSlot::Utf8);
+ if (hasOptionals) {
+ retType = ctx.Expr.MakeType<TOptionalExprType>(retType);
+ }
+
+ input->SetTypeAnn(retType);
+ return IGraphTransformer::TStatus::Ok;
+ }
+
IGraphTransformer::TStatus SubstringWrapper(const TExprNode::TPtr& input, TExprNode::TPtr& output, TExtContext& ctx) {
if (!EnsureArgsCount(*input, 3, ctx.Expr)) {
return IGraphTransformer::TStatus::Error;
@@ -13012,6 +13055,7 @@ template <NKikimr::NUdf::EDataSlot DataSlot>
Functions["WithWorld"] = &WithWorldWrapper;
Functions["Concat"] = &ConcatWrapper;
Functions["AggrConcat"] = &AggrConcatWrapper;
+ ExtFunctions["SqlConcat"] = &SqlConcatWrapper;
ExtFunctions["Substring"] = &SubstringWrapper;
ExtFunctions["Find"] = &FindWrapper;
ExtFunctions["RFind"] = &FindWrapper;
diff --git a/yql/essentials/data/language/sql_functions.json b/yql/essentials/data/language/sql_functions.json
index 074cb24e656..cebadbbef92 100644
--- a/yql/essentials/data/language/sql_functions.json
+++ b/yql/essentials/data/language/sql_functions.json
@@ -220,6 +220,10 @@
"kind": "Normal"
},
{
+ "name": "Concat",
+ "kind": "Normal"
+ },
+ {
"name": "Corr",
"kind": "Agg"
},
diff --git a/yql/essentials/docs/en/builtins/basic.md b/yql/essentials/docs/en/builtins/basic.md
index 994387eaf57..7619c7e9b7f 100644
--- a/yql/essentials/docs/en/builtins/basic.md
+++ b/yql/essentials/docs/en/builtins/basic.md
@@ -90,6 +90,26 @@ SELECT SUBSTRING("abcdefg", 3); -- defg
SELECT SUBSTRING("abcdefg", NULL, 3); -- abc
```
+## Concat {#concat}
+
+Concatenate one or more strings.
+
+#### Signature
+
+```yql
+Concat((String|Utf8)[?], ...)->(String|Utf8)[?]
+```
+
+This function is available since version [2025.04](../changelog/2025.04.md).
+If at least one input string is of type `Optional`, then the result is also of type `Optional`.
+If all input strings are of type `Utf8`, then the result is also of type `Utf8`; otherwise, it is `String`.
+If at least one input string is `NULL`, then the result is also of type `NULL`.
+
+#### Examples
+
+```yql
+SELECT Concat("abc", "de", "f"); -- "abcdef"
+```
## FIND {#find}
diff --git a/yql/essentials/docs/en/changelog/2025.04.md b/yql/essentials/docs/en/changelog/2025.04.md
index f6e20c7092d..42652f7a3bf 100644
--- a/yql/essentials/docs/en/changelog/2025.04.md
+++ b/yql/essentials/docs/en/changelog/2025.04.md
@@ -11,6 +11,7 @@ Added linear types.
* Added `ToDynamicLinear`/`FromDynamicLinear` functions for converting between linear types.
* Added `LinearType`/`DynamicLinearType` functions for constructing linear types. * Added the `LinearTypeHandle`/`DynamicLinearTypeHandle` functions for constructing linear types during code generation.
* Added the `LinearItemType` function for retrieving the parameter type from a linear type.
+* Added `Concat` function for string concatenation.
## Changes in NOT NULL expression
diff --git a/yql/essentials/docs/en/syntax/expressions.md b/yql/essentials/docs/en/syntax/expressions.md
index 7d6ceceb9fd..cc8f9bdeb82 100644
--- a/yql/essentials/docs/en/syntax/expressions.md
+++ b/yql/essentials/docs/en/syntax/expressions.md
@@ -10,6 +10,8 @@ As with other binary operators, if the data on either side is `NULL`, the result
Don't confuse this operator with a logical "or": in SQL, it's denoted by the `OR` keyword. It's also not worth doing concatenation using `+`.
+There is a similar function for this operator [Concat](../builtins/basic.md#concat), which supports an arbitrary number of arguments.
+
#### Examples
```yql
diff --git a/yql/essentials/docs/ru/builtins/basic.md b/yql/essentials/docs/ru/builtins/basic.md
index 5286146d226..ff56f6d994e 100644
--- a/yql/essentials/docs/ru/builtins/basic.md
+++ b/yql/essentials/docs/ru/builtins/basic.md
@@ -109,6 +109,26 @@ SELECT SUBSTRING("abcdefg", 3); -- defg
SELECT SUBSTRING("abcdefg", NULL, 3); -- abc
```
+## Concat {#concat}
+
+Конкатенация одной и более строк.
+
+#### Сигнатура
+
+```yql
+Concat((String|Utf8)[?], ...)->(String|Utf8)[?]
+```
+
+Функция доступна начиная с версии [2025.04](../changelog/2025.04.md).
+Если хотя бы у одной входной строки тип `Optional`, то таким же является и результат.
+Если все входные строки имеют тип `Utf8`, то таким же является и результат, в противном случае - `String`.
+Если хотя бы одна входная строка является `NULL`, то таким же является и результат.
+
+#### Примеры
+
+```yql
+SELECT Concat("abc", "de", "f"); -- "abcdef"
+```
## FIND {#find}
diff --git a/yql/essentials/docs/ru/changelog/2025.04.md b/yql/essentials/docs/ru/changelog/2025.04.md
index 49cf7e82b2c..813940dd483 100644
--- a/yql/essentials/docs/ru/changelog/2025.04.md
+++ b/yql/essentials/docs/ru/changelog/2025.04.md
@@ -12,6 +12,7 @@
* Добавлены функции `LinearType`/`DynamicLinearType` для построения линейных типов.
* Добавлены функции `LinearTypeHandle`/`DynamicLinearTypeHandle` для построения линейных типов во время кодогенерации.
* Добавлена функция `LinearItemType` для получения типа параметра из линейного типа.
+* Добавлена функция `Concat` для конкатенации строк.
## Изменения в выражении NOT NULL
diff --git a/yql/essentials/docs/ru/syntax/expressions.md b/yql/essentials/docs/ru/syntax/expressions.md
index 89687484760..c7825fc340c 100644
--- a/yql/essentials/docs/ru/syntax/expressions.md
+++ b/yql/essentials/docs/ru/syntax/expressions.md
@@ -10,6 +10,8 @@
Не следует путать этот оператор с логическим «или», в SQL оно обозначается ключевым словом `OR`. Также не стоит пытаться делать конкатенацию через `+`.
+Для этого оператора есть аналогичная функция [Concat](../builtins/basic.md#concat), которая поддерживает произвольное число аргументов.
+
#### Примеры
```yql
diff --git a/yql/essentials/minikql/mkql_program_builder.cpp b/yql/essentials/minikql/mkql_program_builder.cpp
index 52e201899b2..2bef8570841 100644
--- a/yql/essentials/minikql/mkql_program_builder.cpp
+++ b/yql/essentials/minikql/mkql_program_builder.cpp
@@ -4807,6 +4807,24 @@ TRuntimeNode TProgramBuilder::Concat(TRuntimeNode data1, TRuntimeNode data2) {
return InvokeBinary(__func__, isOpt1 || isOpt2 ? NewOptionalType(resultType) : resultType, data1, data2);
}
+TRuntimeNode TProgramBuilder::ConcatMany(const TArrayRef<const TRuntimeNode>& args) {
+ MKQL_ENSURE(args.size() >= 1, "Expected at least one argument");
+ if (args.size() == 1) {
+ return args[0];
+ }
+
+ if (args.size() == 2) {
+ return Concat(args[0], args[1]);
+ }
+
+ // TODO make a dedicated callable
+ // split into pairs
+ ui32 midPoint = args.size() / 2;
+ return Concat(
+ ConcatMany(TArrayRef<const TRuntimeNode>(args.data(), args.data() + midPoint)),
+ ConcatMany(TArrayRef<const TRuntimeNode>(args.data() + midPoint, args.end())));
+}
+
TRuntimeNode TProgramBuilder::AggrConcat(TRuntimeNode data1, TRuntimeNode data2) {
MKQL_ENSURE(data1.GetStaticType()->IsSameType(*data2.GetStaticType()), "Operands type mismatch.");
const std::array<TRuntimeNode, 2> args = {{data1, data2}};
diff --git a/yql/essentials/minikql/mkql_program_builder.h b/yql/essentials/minikql/mkql_program_builder.h
index bc02b06ac68..2644b049c91 100644
--- a/yql/essentials/minikql/mkql_program_builder.h
+++ b/yql/essentials/minikql/mkql_program_builder.h
@@ -222,6 +222,7 @@ public:
TRuntimeNode ToDecimal(TRuntimeNode data, ui8 precision, ui8 scale);
TRuntimeNode Concat(TRuntimeNode data1, TRuntimeNode data2);
TRuntimeNode AggrConcat(TRuntimeNode data1, TRuntimeNode data2);
+ TRuntimeNode ConcatMany(const TArrayRef<const TRuntimeNode>& args);
TRuntimeNode Substring(TRuntimeNode data, TRuntimeNode start, TRuntimeNode count);
TRuntimeNode Find(TRuntimeNode haystack, TRuntimeNode needle, TRuntimeNode pos);
TRuntimeNode RFind(TRuntimeNode haystack, TRuntimeNode needle, TRuntimeNode pos);
diff --git a/yql/essentials/providers/common/mkql/yql_provider_mkql.cpp b/yql/essentials/providers/common/mkql/yql_provider_mkql.cpp
index 54a922941f6..760147b6b2c 100644
--- a/yql/essentials/providers/common/mkql/yql_provider_mkql.cpp
+++ b/yql/essentials/providers/common/mkql/yql_provider_mkql.cpp
@@ -656,6 +656,15 @@ TMkqlCommonCallableCompiler::TShared::TShared() {
{"NextValue", &TProgramBuilder::NextValue},
});
+ AddCallable("SqlConcat", [](const TExprNode& node, TMkqlBuildContext& ctx) {
+ TVector<TRuntimeNode> args;
+ for (const auto& child : node.Children()) {
+ args.push_back(MkqlBuildExpr(*child, ctx));
+ }
+
+ return ctx.ProgramBuilder.ConcatMany(args);
+ });
+
AddCallable({"MultiMap", "OrderedMultiMap"}, [](const TExprNode& node, TMkqlBuildContext& ctx) {
const auto arg = MkqlBuildExpr(node.Head(), ctx);
const auto lambda = [&](TRuntimeNode item) { return MkqlBuildWideLambda(node.Tail(), ctx, {item}); };
diff --git a/yql/essentials/sql/v1/builtin.cpp b/yql/essentials/sql/v1/builtin.cpp
index e5ed591beec..17eae94ee4f 100644
--- a/yql/essentials/sql/v1/builtin.cpp
+++ b/yql/essentials/sql/v1/builtin.cpp
@@ -3035,6 +3035,7 @@ struct TBuiltinFuncData {
{"byteat", {"ByteAt", "Normal", BuildNamedArgcBuiltinFactoryCallback<TCallNodeImpl>("ByteAt", 2, 2)}},
{"startswith", {"StartsWith", "Normal", BuildNamedArgcBuiltinFactoryCallback<TCallNodeImpl>("StartsWith", 2, 2)}},
{"endswith", {"EndsWith", "Normal", BuildNamedArgcBuiltinFactoryCallback<TCallNodeImpl>("EndsWith", 2, 2)}},
+ {"concat", {"Concat", "Normal", BuildNamedArgcBuiltinFactoryCallback<TCallNodeImpl>("SqlConcat", 1, -1)}},
// Numeric builtins
{"abs", {"Abs", "Normal", BuildNamedArgcBuiltinFactoryCallback<TCallNodeImpl>("Abs", 1, 1)}},
diff --git a/yql/essentials/tests/sql/minirun/part1/canondata/result.json b/yql/essentials/tests/sql/minirun/part1/canondata/result.json
index bf94623311a..5cb02eb55e3 100644
--- a/yql/essentials/tests/sql/minirun/part1/canondata/result.json
+++ b/yql/essentials/tests/sql/minirun/part1/canondata/result.json
@@ -559,6 +559,20 @@
"uri": "https://{canondata_backend}/1942525/d23808e75f3586fe0f94591ae8678878a50edd66/resource.tar.gz#test.test_expr-as_variant_enum-default.txt-Results_/results.txt"
}
],
+ "test.test[expr-concat_many--Debug]": [
+ {
+ "checksum": "7a4c31257f61a51d15236580c89e6122",
+ "size": 581,
+ "uri": "https://{canondata_backend}/212715/6d9ed45819070f2bb9ee6b84a7d7ce8c50efaa7c/resource.tar.gz#test.test_expr-concat_many--Debug_/opt.yql"
+ }
+ ],
+ "test.test[expr-concat_many--Results]": [
+ {
+ "checksum": "66b26bd358dcff46379f0033958b0d96",
+ "size": 2515,
+ "uri": "https://{canondata_backend}/1784117/a4a7ed2a9293d986efe2868cc4011856a43b2563/resource.tar.gz#test.test_expr-concat_many--Results_/results.txt"
+ }
+ ],
"test.test[expr-expr_named_yql_lambda_quotes-default.txt-Debug]": [
{
"checksum": "9da8a64a8f8feb07730d84fe360e3096",
diff --git a/yql/essentials/tests/sql/sql2yql/canondata/result.json b/yql/essentials/tests/sql/sql2yql/canondata/result.json
index c30e4b85e42..17df9327bdd 100644
--- a/yql/essentials/tests/sql/sql2yql/canondata/result.json
+++ b/yql/essentials/tests/sql/sql2yql/canondata/result.json
@@ -2918,6 +2918,13 @@
"uri": "https://{canondata_backend}/1937150/ec0019724df75083b0e89cab22f57e10ef36744e/resource.tar.gz#test_sql2yql.test_expr-common_type_for_resource_and_data_/sql.yql"
}
],
+ "test_sql2yql.test[expr-concat_many]": [
+ {
+ "checksum": "d9e698da5ede3c7520b7f0d122ab8566",
+ "size": 1935,
+ "uri": "https://{canondata_backend}/1936842/92e2458bff4de81de9f2c44dae4a80b1a84e7c0c/resource.tar.gz#test_sql2yql.test_expr-concat_many_/sql.yql"
+ }
+ ],
"test_sql2yql.test[expr-container_comp]": [
{
"checksum": "6aaee0040ccee9661847c7edf6f681bb",
@@ -10918,6 +10925,11 @@
"uri": "file://test_sql_format.test_expr-common_type_for_resource_and_data_/formatted.sql"
}
],
+ "test_sql_format.test[expr-concat_many]": [
+ {
+ "uri": "file://test_sql_format.test_expr-concat_many_/formatted.sql"
+ }
+ ],
"test_sql_format.test[expr-container_comp]": [
{
"uri": "file://test_sql_format.test_expr-container_comp_/formatted.sql"
diff --git a/yql/essentials/tests/sql/sql2yql/canondata/test_sql_format.test_expr-concat_many_/formatted.sql b/yql/essentials/tests/sql/sql2yql/canondata/test_sql_format.test_expr-concat_many_/formatted.sql
new file mode 100644
index 00000000000..0b837768499
--- /dev/null
+++ b/yql/essentials/tests/sql/sql2yql/canondata/test_sql_format.test_expr-concat_many_/formatted.sql
@@ -0,0 +1,8 @@
+SELECT
+ concat(NULL),
+ concat('aa'),
+ concat('aa', 'bb', 'cc'),
+ concat(just('aa'u), just('bb')),
+ concat('aa', 'bb'u),
+ concat('aa'u, just('bb'u))
+;
diff --git a/yql/essentials/tests/sql/suites/expr/concat_many.cfg b/yql/essentials/tests/sql/suites/expr/concat_many.cfg
new file mode 100644
index 00000000000..7e882f1da85
--- /dev/null
+++ b/yql/essentials/tests/sql/suites/expr/concat_many.cfg
@@ -0,0 +1,2 @@
+langver 2025.04
+
diff --git a/yql/essentials/tests/sql/suites/expr/concat_many.yql b/yql/essentials/tests/sql/suites/expr/concat_many.yql
new file mode 100644
index 00000000000..58ef1cc243a
--- /dev/null
+++ b/yql/essentials/tests/sql/suites/expr/concat_many.yql
@@ -0,0 +1,5 @@
+select concat(null),concat('aa'),
+ concat('aa','bb','cc'),concat(just('aa'u),just('bb')),
+ concat('aa','bb'u),concat('aa'u,just('bb'u));
+
+