diff --git a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java index 14f058a75d0..4dd267984bf 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java @@ -72,6 +72,7 @@ public enum BuiltinFunctionName { ARRAY_LENGTH(FunctionName.of("array_length")), ARRAY_SLICE(FunctionName.of("array_slice"), true), ARRAY_COMPACT(FunctionName.of("array_compact")), + ARRAY_TO_CSV(FunctionName.of("array_to_csv")), MAP_APPEND(FunctionName.of("map_append"), true), MAP_CONCAT(FunctionName.of("map_concat"), true), MAP_REMOVE(FunctionName.of("map_remove"), true), diff --git a/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/ArrayToCsvFunctionImpl.java b/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/ArrayToCsvFunctionImpl.java new file mode 100644 index 00000000000..8dc0855f34a --- /dev/null +++ b/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/ArrayToCsvFunctionImpl.java @@ -0,0 +1,109 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.expression.function.CollectionUDF; + +import java.util.List; +import org.apache.calcite.adapter.enumerable.NotNullImplementor; +import org.apache.calcite.adapter.enumerable.NullPolicy; +import org.apache.calcite.adapter.enumerable.RexToLixTranslator; +import org.apache.calcite.linq4j.tree.Expression; +import org.apache.calcite.linq4j.tree.Expressions; +import org.apache.calcite.linq4j.tree.Types; +import org.apache.calcite.rel.type.RelDataTypeFactory; +import org.apache.calcite.rex.RexCall; +import org.apache.calcite.sql.type.CompositeOperandTypeChecker; +import org.apache.calcite.sql.type.OperandTypes; +import org.apache.calcite.sql.type.SqlReturnTypeInference; +import org.apache.calcite.sql.type.SqlTypeFamily; +import org.apache.calcite.sql.type.SqlTypeName; +import org.opensearch.sql.expression.function.ImplementorUDF; +import org.opensearch.sql.expression.function.UDFOperandMetadata; + +/** ARRAY_TO_CSV function implementation that converts an array to a CSV string. */ +public class ArrayToCsvFunctionImpl extends ImplementorUDF { + + public ArrayToCsvFunctionImpl() { + super(new ArrayToCsvImplementor(), NullPolicy.ARG0); + } + + @Override + public SqlReturnTypeInference getReturnTypeInference() { + return sqlOperatorBinding -> { + RelDataTypeFactory typeFactory = sqlOperatorBinding.getTypeFactory(); + return typeFactory.createTypeWithNullability( + typeFactory.createSqlType(SqlTypeName.VARCHAR), true); + }; + } + + @Override + public UDFOperandMetadata getOperandMetadata() { + // Accept ARRAY as first argument, optional STRING as second argument (delimiter) + return UDFOperandMetadata.wrap( + (CompositeOperandTypeChecker) + OperandTypes.family(SqlTypeFamily.ARRAY) + .or(OperandTypes.family(SqlTypeFamily.ARRAY, SqlTypeFamily.CHARACTER))); + } + + public static class ArrayToCsvImplementor implements NotNullImplementor { + @Override + public Expression implement( + RexToLixTranslator translator, RexCall call, List translatedOperands) { + // Handle both 1-argument (with default delimiter) and 2-argument cases + if (translatedOperands.size() == 1) { + // ARRAY_TO_CSV(array) - use default delimiter "," + return Expressions.call( + Types.lookupMethod( + ArrayToCsvFunctionImpl.class, "arrayToCsv", List.class, String.class), + translatedOperands.get(0), + Expressions.constant(",")); + } else if (translatedOperands.size() == 2) { + // ARRAY_TO_CSV(array, delimiter) + return Expressions.call( + Types.lookupMethod( + ArrayToCsvFunctionImpl.class, "arrayToCsv", List.class, String.class), + translatedOperands.get(0), + translatedOperands.get(1)); + } else { + throw new IllegalArgumentException( + "ARRAY_TO_CSV expects 1 or 2 arguments, got " + translatedOperands.size()); + } + } + } + + /** + * Converts an array to a CSV string. + * + * @param array The array to convert + * @param delimiter The delimiter to use for joining values + * @return CSV string representation of the array + */ + public static String arrayToCsv(List array, String delimiter) { + if (array == null) { + return null; + } + + if (delimiter == null) { + delimiter = ","; + } + + if (array.isEmpty()) { + return ""; + } + + StringBuilder result = new StringBuilder(); + for (int i = 0; i < array.size(); i++) { + if (i > 0) { + result.append(delimiter); + } + Object element = array.get(i); + if (element != null) { + result.append(element.toString()); + } + } + + return result.toString(); + } +} diff --git a/core/src/main/java/org/opensearch/sql/expression/function/PPLBuiltinOperators.java b/core/src/main/java/org/opensearch/sql/expression/function/PPLBuiltinOperators.java index 0a5b0fe0e03..8bbca5199f1 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/PPLBuiltinOperators.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/PPLBuiltinOperators.java @@ -43,6 +43,7 @@ import org.opensearch.sql.data.type.ExprCoreType; import org.opensearch.sql.expression.datetime.DateTimeFunctions; import org.opensearch.sql.expression.function.CollectionUDF.ArrayFunctionImpl; +import org.opensearch.sql.expression.function.CollectionUDF.ArrayToCsvFunctionImpl; import org.opensearch.sql.expression.function.CollectionUDF.ExistsFunctionImpl; import org.opensearch.sql.expression.function.CollectionUDF.FilterFunctionImpl; import org.opensearch.sql.expression.function.CollectionUDF.ForallFunctionImpl; @@ -400,6 +401,7 @@ public class PPLBuiltinOperators extends ReflectiveSqlOperatorTable { public static final SqlOperator FORALL = new ForallFunctionImpl().toUDF("forall"); public static final SqlOperator EXISTS = new ExistsFunctionImpl().toUDF("exists"); public static final SqlOperator ARRAY = new ArrayFunctionImpl().toUDF("array"); + public static final SqlOperator ARRAY_TO_CSV = new ArrayToCsvFunctionImpl().toUDF("array_to_csv"); public static final SqlOperator MAP_APPEND = new MapAppendFunctionImpl().toUDF("map_append"); public static final SqlOperator MAP_REMOVE = new MapRemoveFunctionImpl().toUDF("MAP_REMOVE"); public static final SqlOperator MVAPPEND = new MVAppendFunctionImpl().toUDF("mvappend"); diff --git a/core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java b/core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java index 849c60fe4eb..2ecb5c486ed 100644 --- a/core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java +++ b/core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java @@ -19,6 +19,7 @@ import static org.opensearch.sql.expression.function.BuiltinFunctionName.ARRAY_COMPACT; import static org.opensearch.sql.expression.function.BuiltinFunctionName.ARRAY_LENGTH; import static org.opensearch.sql.expression.function.BuiltinFunctionName.ARRAY_SLICE; +import static org.opensearch.sql.expression.function.BuiltinFunctionName.ARRAY_TO_CSV; import static org.opensearch.sql.expression.function.BuiltinFunctionName.ASCII; import static org.opensearch.sql.expression.function.BuiltinFunctionName.ASIN; import static org.opensearch.sql.expression.function.BuiltinFunctionName.ATAN; @@ -1062,6 +1063,7 @@ void populate() { registerOperator(ARRAY_LENGTH, SqlLibraryOperators.ARRAY_LENGTH); registerOperator(ARRAY_SLICE, SqlLibraryOperators.ARRAY_SLICE); registerOperator(ARRAY_COMPACT, SqlLibraryOperators.ARRAY_COMPACT); + registerOperator(ARRAY_TO_CSV, PPLBuiltinOperators.ARRAY_TO_CSV); registerOperator(FORALL, PPLBuiltinOperators.FORALL); registerOperator(EXISTS, PPLBuiltinOperators.EXISTS); registerOperator(FILTER, PPLBuiltinOperators.FILTER); diff --git a/core/src/test/java/org/opensearch/sql/expression/function/CollectionUDF/ArrayToCsvFunctionImplTest.java b/core/src/test/java/org/opensearch/sql/expression/function/CollectionUDF/ArrayToCsvFunctionImplTest.java new file mode 100644 index 00000000000..be91a84a498 --- /dev/null +++ b/core/src/test/java/org/opensearch/sql/expression/function/CollectionUDF/ArrayToCsvFunctionImplTest.java @@ -0,0 +1,135 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.expression.function.CollectionUDF; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +class ArrayToCsvFunctionImplTest { + + @Test + void testArrayToCsvWithDefaultDelimiter() { + List array = Arrays.asList("GET", "READ", "WRITE"); + String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ","); + assertEquals("GET,READ,WRITE", result); + } + + @Test + void testArrayToCsvWithCustomDelimiter() { + List array = Arrays.asList("GET", "READ", "WRITE"); + String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ", "); + assertEquals("GET, READ, WRITE", result); + } + + @Test + void testArrayToCsvWithPipeDelimiter() { + List array = Arrays.asList("GET", "READ", "WRITE"); + String result = ArrayToCsvFunctionImpl.arrayToCsv(array, " | "); + assertEquals("GET | READ | WRITE", result); + } + + @Test + void testArrayToCsvWithEmptyArray() { + List array = Collections.emptyList(); + String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ","); + assertEquals("", result); + } + + @Test + void testArrayToCsvWithNullArray() { + String result = ArrayToCsvFunctionImpl.arrayToCsv(null, ","); + assertNull(result); + } + + @Test + void testArrayToCsvWithNullElements() { + List array = Arrays.asList("GET", null, "WRITE"); + String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ","); + assertEquals("GET,,WRITE", result); + } + + @Test + void testArrayToCsvWithSingleElement() { + List array = Arrays.asList("GET"); + String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ","); + assertEquals("GET", result); + } + + @Test + void testArrayToCsvWithNumbers() { + List array = Arrays.asList(1, 2, 3); + String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ","); + assertEquals("1,2,3", result); + } + + @Test + void testArrayToCsvWithMixedTypes() { + List array = Arrays.asList("GET", 123, true); + String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ","); + assertEquals("GET,123,true", result); + } + + @Test + void testArrayToCsvWithNullDelimiter() { + List array = Arrays.asList("a", "b", "c"); + String result = ArrayToCsvFunctionImpl.arrayToCsv(array, null); + assertEquals("a,b,c", result); + } + + @Test + void testArrayToCsvWithNullDelimiterAndSingleElement() { + List array = Arrays.asList("GET"); + String result = ArrayToCsvFunctionImpl.arrayToCsv(array, null); + assertEquals("GET", result); + } + + @Test + void testArrayToCsvWithNullDelimiterAndEmptyArray() { + List array = Collections.emptyList(); + String result = ArrayToCsvFunctionImpl.arrayToCsv(array, null); + assertEquals("", result); + } + + @Test + void testArrayToCsvWithMultipleNullElements() { + List array = Arrays.asList("a", null, null, "b"); + String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ","); + assertEquals("a,,,b", result); + } + + @Test + void testArrayToCsvWithLeadingNullElement() { + List array = Arrays.asList(null, "a", "b"); + String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ","); + assertEquals(",a,b", result); + } + + @Test + void testArrayToCsvWithTrailingNullElement() { + List array = Arrays.asList("a", "b", null); + String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ","); + assertEquals("a,b,", result); + } + + @Test + void testArrayToCsvWithAllNullElements() { + List array = Arrays.asList(null, null, null); + String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ","); + assertEquals(",,", result); + } + + @Test + void testArrayToCsvWithNullElementsAndCustomDelimiter() { + List array = Arrays.asList("GET", null, "WRITE"); + String result = ArrayToCsvFunctionImpl.arrayToCsv(array, " | "); + assertEquals("GET | | WRITE", result); + } +} diff --git a/docs/user/ppl/functions/array_to_csv.md b/docs/user/ppl/functions/array_to_csv.md new file mode 100644 index 00000000000..8d8e552504d --- /dev/null +++ b/docs/user/ppl/functions/array_to_csv.md @@ -0,0 +1,202 @@ +# ARRAY_TO_CSV + +**Usage**: `array_to_csv(array, [delimiter])` + +Converts an array to a comma-separated values (CSV) string representation. All array elements are converted to their string representation and joined using the specified delimiter. + +**Parameters**: + +- `array` (Required): An array expression of any supported data type to convert to CSV. +- `delimiter` (Optional): The string to use as a separator between array elements. Defaults to comma (","). + +**Return type**: `STRING` + +## Example + +The following example converts an array to CSV with the default comma delimiter: + +```ppl +source=people +| eval result = array_to_csv(array('apple', 'banana', 'cherry')) +| fields result +| head 1 +``` + +The query returns the following results: + +```text +fetched rows / total rows = 1/1 ++-------------------------+ +| result | +|-------------------------| +| apple,banana,cherry | ++-------------------------+ +``` + +The following example uses a custom delimiter: + +```ppl +source=people +| eval result = array_to_csv(array('a', 'b', 'c'), '|') +| fields result +| head 1 +``` + +The query returns the following results: + +```text +fetched rows / total rows = 1/1 ++---------+ +| result | +|---------| +| a|b|c | ++---------+ +``` + +The following example converts a numeric array: + +```ppl +source=people +| eval result = array_to_csv(array(1, 2, 3, 4)) +| fields result +| head 1 +``` + +The query returns the following results: + +```text +fetched rows / total rows = 1/1 ++---------+ +| result | +|---------| +| 1,2,3,4 | ++---------+ +``` + +The following example handles mixed data types: + +```ppl +source=people +| eval result = array_to_csv(array('text', 123, 45.67), ';') +| fields result +| head 1 +``` + +The query returns the following results: + +```text +fetched rows / total rows = 1/1 ++----------------+ +| result | +|----------------| +| text;123;45.67 | ++----------------+ +``` + +The following example uses multi-character delimiters: + +```ppl +source=people +| eval result = array_to_csv(array('apple', 'banana', 'cherry'), ' -> ') +| fields result +| head 1 +``` + +The query returns the following results: + +```text +fetched rows / total rows = 1/1 ++---------------------------+ +| result | +|---------------------------| +| apple -> banana -> cherry | ++---------------------------+ +``` + +The following example uses arrays created from real database fields: + +```ppl +source=accounts +| eval names_array = array(firstname, lastname) +| eval full_name = array_to_csv(names_array, ' ') +| fields firstname, lastname, full_name +| head 1 +``` + +The query returns the following results: + +```text +fetched rows / total rows = 1/1 ++------------+-----------+--------------+ +| firstname | lastname | full_name | +|------------|-----------|--------------| +| Amber | Duke | Amber Duke | ++------------+-----------+--------------+ +``` + +The following example shows behavior with empty arrays: + +```ppl +source=people +| eval result = array_to_csv(array()) +| fields result +| head 1 +``` + +The query returns the following results: + +```text +fetched rows / total rows = 1/1 ++--------+ +| result | +|--------| +| | ++--------+ +``` + +The following example shows behavior with single element arrays: + +```ppl +source=people +| eval result = array_to_csv(array('single')) +| fields result +| head 1 +``` + +The query returns the following results: + +```text +fetched rows / total rows = 1/1 ++--------+ +| result | +|--------| +| single | ++--------+ +``` + +The following example demonstrates null element handling: + +```ppl +source=people +| eval result = array_to_csv(array('a', null, 'c')) +| fields result +| head 1 +``` + +The query returns the following results: + +```text +fetched rows / total rows = 1/1 ++--------+ +| result | +|--------| +| a,,c | ++--------+ +``` + +## Limitations + +- The function converts all array elements to their string representation +- Null array elements are represented as empty strings in the output +- If the input array is null, the function returns null +- If the delimiter is null, it defaults to comma (",") \ No newline at end of file diff --git a/docs/user/ppl/functions/collection.md b/docs/user/ppl/functions/collection.md index 006a49ef6d5..a434a2607af 100644 --- a/docs/user/ppl/functions/collection.md +++ b/docs/user/ppl/functions/collection.md @@ -91,6 +91,81 @@ fetched rows / total rows = 1/1 | 3 | +--------+ ``` + +## ARRAY_TO_CSV + +**Usage**: `array_to_csv(array, [delimiter])` + +Converts an array to a comma-separated values (CSV) string representation. All array elements are converted to their string representation and joined using the specified delimiter. + +**Parameters**: + +- `array` (Required): An array expression of any supported data type to convert to CSV. +- `delimiter` (Optional): The string to use as a separator between array elements. Defaults to comma (","). + +**Return type**: `STRING` + +#### Example + +The following example converts an array to CSV with the default comma delimiter: + +```ppl +source=people +| eval result = array_to_csv(array('apple', 'banana', 'cherry')) +| fields result +| head 1 +``` + +The query returns the following results: + +```text +fetched rows / total rows = 1/1 ++-------------------------+ +| result | +|-------------------------| +| apple,banana,cherry | ++-------------------------+ +``` + +The following example uses a custom delimiter: + +```ppl +source=people +| eval result = array_to_csv(array('a', 'b', 'c'), '|') +| fields result +| head 1 +``` + +The query returns the following results: + +```text +fetched rows / total rows = 1/1 ++---------+ +| result | +|---------| +| a|b|c | ++---------+ +``` + +The following example handles mixed data types: + +```ppl +source=people +| eval result = array_to_csv(array('text', 123, 45.67), ';') +| fields result +| head 1 +``` + +The query returns the following results: + +```text +fetched rows / total rows = 1/1 ++----------------+ +| result | +|----------------| +| text;123;45.67 | ++----------------+ +``` ## FORALL diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteArrayFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteArrayFunctionIT.java index 26e8ecf73f6..afc4b579328 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteArrayFunctionIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteArrayFunctionIT.java @@ -867,4 +867,118 @@ public void testMvmapWithEvalFieldReference() throws IOException { verifySchema(actual, schema("result", "array")); verifyDataRows(actual, rows(List.of(10, 20, 30))); } + + @Test + public void testArrayToCsvBasic() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval arr = array('apple', 'banana', 'cherry'), result =" + + " array_to_csv(arr) | head 1 | fields result", + TEST_INDEX_BANK)); + + verifySchema(actual, schema("result", "string")); + verifyDataRows(actual, rows("apple,banana,cherry")); + } + + @Test + public void testArrayToCsvWithCustomDelimiter() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval arr = array('apple', 'banana', 'cherry'), result =" + + " array_to_csv(arr, '|') | head 1 | fields result", + TEST_INDEX_BANK)); + + verifySchema(actual, schema("result", "string")); + verifyDataRows(actual, rows("apple|banana|cherry")); + } + + @Test + public void testArrayToCsvWithNumbers() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval arr = array(1, 2, 3, 4), result = array_to_csv(arr) | head 1 |" + + " fields result", + TEST_INDEX_BANK)); + + verifySchema(actual, schema("result", "string")); + verifyDataRows(actual, rows("1,2,3,4")); + } + + @Test + public void testArrayToCsvWithMixedTypes() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval arr = array('text', 123, 45.67), result = array_to_csv(arr, ';')" + + " | head 1 | fields result", + TEST_INDEX_BANK)); + + verifySchema(actual, schema("result", "string")); + verifyDataRows(actual, rows("text;123;45.67")); + } + + @Test + public void testArrayToCsvWithEmptyArray() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval arr = array(), result = array_to_csv(arr) | head 1 | fields" + + " result", + TEST_INDEX_BANK)); + + verifySchema(actual, schema("result", "string")); + verifyDataRows(actual, rows("")); + } + + @Test + public void testArrayToCsvWithSingleElement() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval arr = array('single'), result = array_to_csv(arr) | head 1 |" + + " fields result", + TEST_INDEX_BANK)); + + verifySchema(actual, schema("result", "string")); + verifyDataRows(actual, rows("single")); + } + + @Test + public void testArrayToCsvWithSpecialDelimiters() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval arr = array('a', 'b', 'c'), result = array_to_csv(arr, ' -> ') |" + + " head 1 | fields result", + TEST_INDEX_BANK)); + + verifySchema(actual, schema("result", "string")); + verifyDataRows(actual, rows("a -> b -> c")); + } + + @Test + public void testArrayToCsvWithRealFields() throws IOException { + // Test array_to_csv with arrays created from real fields + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval names_array = array(firstname, lastname), result =" + + " array_to_csv(names_array, ' ') | fields firstname, lastname, result | head" + + " 1", + TEST_INDEX_BANK)); + + verifySchema( + actual, + schema("firstname", "string"), + schema("lastname", "string"), + schema("result", "string")); + // Verify that array_to_csv correctly joins the firstname and lastname fields + JSONArray dataRows = actual.getJSONArray("datarows"); + assertTrue(dataRows.length() > 0); + JSONArray firstRow = dataRows.getJSONArray(0); + assertEquals(firstRow.getString(0) + " " + firstRow.getString(1), firstRow.getString(2)); + } } diff --git a/ppl/src/main/antlr/OpenSearchPPLLexer.g4 b/ppl/src/main/antlr/OpenSearchPPLLexer.g4 index 4bc69a8f295..b42729e869f 100644 --- a/ppl/src/main/antlr/OpenSearchPPLLexer.g4 +++ b/ppl/src/main/antlr/OpenSearchPPLLexer.g4 @@ -480,6 +480,7 @@ ISBLANK: 'ISBLANK'; // COLLECTION FUNCTIONS ARRAY: 'ARRAY'; ARRAY_LENGTH: 'ARRAY_LENGTH'; +ARRAY_TO_CSV: 'ARRAY_TO_CSV'; MVAPPEND: 'MVAPPEND'; MVJOIN: 'MVJOIN'; MVINDEX: 'MVINDEX'; diff --git a/ppl/src/main/antlr/OpenSearchPPLParser.g4 b/ppl/src/main/antlr/OpenSearchPPLParser.g4 index bcaaa105774..628105d6d54 100644 --- a/ppl/src/main/antlr/OpenSearchPPLParser.g4 +++ b/ppl/src/main/antlr/OpenSearchPPLParser.g4 @@ -1231,6 +1231,7 @@ geoipFunctionName collectionFunctionName : ARRAY | ARRAY_LENGTH + | ARRAY_TO_CSV | MVAPPEND | MVJOIN | MVINDEX