Skip to content

Commit 1a85890

Browse files
author
Isha Gupta
committed
Add ARRAY_TO_CSV helper function
Signed-off-by: Isha Gupta <igupta24@apple.com>
1 parent da1410b commit 1a85890

7 files changed

Lines changed: 253 additions & 0 deletions

File tree

core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public enum BuiltinFunctionName {
7272
ARRAY_LENGTH(FunctionName.of("array_length")),
7373
ARRAY_SLICE(FunctionName.of("array_slice"), true),
7474
ARRAY_COMPACT(FunctionName.of("array_compact")),
75+
ARRAY_TO_CSV(FunctionName.of("array_to_csv")),
7576
MAP_APPEND(FunctionName.of("map_append"), true),
7677
MAP_CONCAT(FunctionName.of("map_concat"), true),
7778
MAP_REMOVE(FunctionName.of("map_remove"), true),
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.expression.function.CollectionUDF;
7+
8+
import java.util.List;
9+
import org.apache.calcite.adapter.enumerable.NotNullImplementor;
10+
import org.apache.calcite.adapter.enumerable.NullPolicy;
11+
import org.apache.calcite.adapter.enumerable.RexToLixTranslator;
12+
import org.apache.calcite.linq4j.tree.Expression;
13+
import org.apache.calcite.linq4j.tree.Expressions;
14+
import org.apache.calcite.linq4j.tree.Types;
15+
import org.apache.calcite.rel.type.RelDataTypeFactory;
16+
import org.apache.calcite.rex.RexCall;
17+
import org.apache.calcite.sql.type.CompositeOperandTypeChecker;
18+
import org.apache.calcite.sql.type.OperandTypes;
19+
import org.apache.calcite.sql.type.SqlReturnTypeInference;
20+
import org.apache.calcite.sql.type.SqlTypeFamily;
21+
import org.apache.calcite.sql.type.SqlTypeName;
22+
import org.opensearch.sql.expression.function.ImplementorUDF;
23+
import org.opensearch.sql.expression.function.UDFOperandMetadata;
24+
25+
/**
26+
* ARRAY_TO_CSV function implementation that converts an array to a CSV string.
27+
*/
28+
public class ArrayToCsvFunctionImpl extends ImplementorUDF {
29+
30+
public ArrayToCsvFunctionImpl() {
31+
super(new ArrayToCsvImplementor(), NullPolicy.ARG0);
32+
}
33+
34+
@Override
35+
public SqlReturnTypeInference getReturnTypeInference() {
36+
return sqlOperatorBinding -> {
37+
RelDataTypeFactory typeFactory = sqlOperatorBinding.getTypeFactory();
38+
return typeFactory.createTypeWithNullability(
39+
typeFactory.createSqlType(SqlTypeName.VARCHAR), true);
40+
};
41+
}
42+
43+
@Override
44+
public UDFOperandMetadata getOperandMetadata() {
45+
// Accept ARRAY as first argument, optional STRING as second argument (delimiter)
46+
return UDFOperandMetadata.wrap(
47+
(CompositeOperandTypeChecker)
48+
OperandTypes.family(SqlTypeFamily.ARRAY)
49+
.or(OperandTypes.family(SqlTypeFamily.ARRAY, SqlTypeFamily.CHARACTER)));
50+
}
51+
52+
public static class ArrayToCsvImplementor implements NotNullImplementor {
53+
@Override
54+
public Expression implement(
55+
RexToLixTranslator translator, RexCall call, List<Expression> translatedOperands) {
56+
// Handle both 1-argument (with default delimiter) and 2-argument cases
57+
if (translatedOperands.size() == 1) {
58+
// ARRAY_TO_CSV(array) - use default delimiter ","
59+
return Expressions.call(
60+
Types.lookupMethod(
61+
ArrayToCsvFunctionImpl.class, "arrayToCsv", List.class, String.class),
62+
translatedOperands.get(0),
63+
Expressions.constant(","));
64+
} else if (translatedOperands.size() == 2) {
65+
// ARRAY_TO_CSV(array, delimiter)
66+
return Expressions.call(
67+
Types.lookupMethod(
68+
ArrayToCsvFunctionImpl.class, "arrayToCsv", List.class, String.class),
69+
translatedOperands.get(0),
70+
translatedOperands.get(1));
71+
} else {
72+
throw new IllegalArgumentException(
73+
"ARRAY_TO_CSV expects 1 or 2 arguments, got " + translatedOperands.size());
74+
}
75+
}
76+
}
77+
78+
/**
79+
* Converts an array to a CSV string.
80+
*
81+
* @param array The array to convert
82+
* @param delimiter The delimiter to use for joining values
83+
* @return CSV string representation of the array
84+
*/
85+
public static String arrayToCsv(List<Object> array, String delimiter) {
86+
if (array == null) {
87+
return null;
88+
}
89+
90+
if (delimiter == null) {
91+
delimiter = ",";
92+
}
93+
94+
if (array.isEmpty()) {
95+
return "";
96+
}
97+
98+
StringBuilder result = new StringBuilder();
99+
for (int i = 0; i < array.size(); i++) {
100+
if (i > 0) {
101+
result.append(delimiter);
102+
}
103+
Object element = array.get(i);
104+
if (element != null) {
105+
result.append(element.toString());
106+
}
107+
}
108+
109+
return result.toString();
110+
}
111+
}

core/src/main/java/org/opensearch/sql/expression/function/PPLBuiltinOperators.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.opensearch.sql.data.type.ExprCoreType;
4444
import org.opensearch.sql.expression.datetime.DateTimeFunctions;
4545
import org.opensearch.sql.expression.function.CollectionUDF.ArrayFunctionImpl;
46+
import org.opensearch.sql.expression.function.CollectionUDF.ArrayToCsvFunctionImpl;
4647
import org.opensearch.sql.expression.function.CollectionUDF.ExistsFunctionImpl;
4748
import org.opensearch.sql.expression.function.CollectionUDF.FilterFunctionImpl;
4849
import org.opensearch.sql.expression.function.CollectionUDF.ForallFunctionImpl;
@@ -400,6 +401,7 @@ public class PPLBuiltinOperators extends ReflectiveSqlOperatorTable {
400401
public static final SqlOperator FORALL = new ForallFunctionImpl().toUDF("forall");
401402
public static final SqlOperator EXISTS = new ExistsFunctionImpl().toUDF("exists");
402403
public static final SqlOperator ARRAY = new ArrayFunctionImpl().toUDF("array");
404+
public static final SqlOperator ARRAY_TO_CSV = new ArrayToCsvFunctionImpl().toUDF("array_to_csv");
403405
public static final SqlOperator MAP_APPEND = new MapAppendFunctionImpl().toUDF("map_append");
404406
public static final SqlOperator MAP_REMOVE = new MapRemoveFunctionImpl().toUDF("MAP_REMOVE");
405407
public static final SqlOperator MVAPPEND = new MVAppendFunctionImpl().toUDF("mvappend");

core/src/main/java/org/opensearch/sql/expression/function/PPLFuncImpTable.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static org.opensearch.sql.expression.function.BuiltinFunctionName.ARRAY_COMPACT;
2020
import static org.opensearch.sql.expression.function.BuiltinFunctionName.ARRAY_LENGTH;
2121
import static org.opensearch.sql.expression.function.BuiltinFunctionName.ARRAY_SLICE;
22+
import static org.opensearch.sql.expression.function.BuiltinFunctionName.ARRAY_TO_CSV;
2223
import static org.opensearch.sql.expression.function.BuiltinFunctionName.ASCII;
2324
import static org.opensearch.sql.expression.function.BuiltinFunctionName.ASIN;
2425
import static org.opensearch.sql.expression.function.BuiltinFunctionName.ATAN;
@@ -1062,6 +1063,7 @@ void populate() {
10621063
registerOperator(ARRAY_LENGTH, SqlLibraryOperators.ARRAY_LENGTH);
10631064
registerOperator(ARRAY_SLICE, SqlLibraryOperators.ARRAY_SLICE);
10641065
registerOperator(ARRAY_COMPACT, SqlLibraryOperators.ARRAY_COMPACT);
1066+
registerOperator(ARRAY_TO_CSV, PPLBuiltinOperators.ARRAY_TO_CSV);
10651067
registerOperator(FORALL, PPLBuiltinOperators.FORALL);
10661068
registerOperator(EXISTS, PPLBuiltinOperators.EXISTS);
10671069
registerOperator(FILTER, PPLBuiltinOperators.FILTER);
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.expression.function.CollectionUDF;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
import static org.junit.jupiter.api.Assertions.assertNull;
10+
11+
import java.util.Arrays;
12+
import java.util.Collections;
13+
import java.util.List;
14+
import org.junit.jupiter.api.Test;
15+
16+
class ArrayToCsvFunctionImplTest {
17+
18+
@Test
19+
void testArrayToCsvWithDefaultDelimiter() {
20+
List<Object> array = Arrays.asList("GET", "READ", "WRITE");
21+
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ",");
22+
assertEquals("GET,READ,WRITE", result);
23+
}
24+
25+
@Test
26+
void testArrayToCsvWithCustomDelimiter() {
27+
List<Object> array = Arrays.asList("GET", "READ", "WRITE");
28+
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ", ");
29+
assertEquals("GET, READ, WRITE", result);
30+
}
31+
32+
@Test
33+
void testArrayToCsvWithPipeDelimiter() {
34+
List<Object> array = Arrays.asList("GET", "READ", "WRITE");
35+
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, " | ");
36+
assertEquals("GET | READ | WRITE", result);
37+
}
38+
39+
@Test
40+
void testArrayToCsvWithEmptyArray() {
41+
List<Object> array = Collections.emptyList();
42+
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ",");
43+
assertEquals("", result);
44+
}
45+
46+
@Test
47+
void testArrayToCsvWithNullArray() {
48+
String result = ArrayToCsvFunctionImpl.arrayToCsv(null, ",");
49+
assertNull(result);
50+
}
51+
52+
@Test
53+
void testArrayToCsvWithNullElements() {
54+
List<Object> array = Arrays.asList("GET", null, "WRITE");
55+
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ",");
56+
assertEquals("GET,,WRITE", result);
57+
}
58+
59+
@Test
60+
void testArrayToCsvWithSingleElement() {
61+
List<Object> array = Arrays.asList("GET");
62+
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ",");
63+
assertEquals("GET", result);
64+
}
65+
66+
@Test
67+
void testArrayToCsvWithNumbers() {
68+
List<Object> array = Arrays.asList(1, 2, 3);
69+
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ",");
70+
assertEquals("1,2,3", result);
71+
}
72+
73+
@Test
74+
void testArrayToCsvWithMixedTypes() {
75+
List<Object> array = Arrays.asList("GET", 123, true);
76+
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ",");
77+
assertEquals("GET,123,true", result);
78+
}
79+
80+
@Test
81+
void testArrayToCsvWithNullDelimiter() {
82+
List<Object> array = Arrays.asList("a", "b", "c");
83+
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, null);
84+
assertEquals("a,b,c", result);
85+
}
86+
87+
@Test
88+
void testArrayToCsvWithNullDelimiterAndSingleElement() {
89+
List<Object> array = Arrays.asList("GET");
90+
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, null);
91+
assertEquals("GET", result);
92+
}
93+
94+
@Test
95+
void testArrayToCsvWithNullDelimiterAndEmptyArray() {
96+
List<Object> array = Collections.emptyList();
97+
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, null);
98+
assertEquals("", result);
99+
}
100+
101+
@Test
102+
void testArrayToCsvWithMultipleNullElements() {
103+
List<Object> array = Arrays.asList("a", null, null, "b");
104+
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ",");
105+
assertEquals("a,,,b", result);
106+
}
107+
108+
@Test
109+
void testArrayToCsvWithLeadingNullElement() {
110+
List<Object> array = Arrays.asList(null, "a", "b");
111+
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ",");
112+
assertEquals(",a,b", result);
113+
}
114+
115+
@Test
116+
void testArrayToCsvWithTrailingNullElement() {
117+
List<Object> array = Arrays.asList("a", "b", null);
118+
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ",");
119+
assertEquals("a,b,", result);
120+
}
121+
122+
@Test
123+
void testArrayToCsvWithAllNullElements() {
124+
List<Object> array = Arrays.asList(null, null, null);
125+
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ",");
126+
assertEquals(",,", result);
127+
}
128+
129+
@Test
130+
void testArrayToCsvWithNullElementsAndCustomDelimiter() {
131+
List<Object> array = Arrays.asList("GET", null, "WRITE");
132+
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, " | ");
133+
assertEquals("GET | | WRITE", result);
134+
}
135+
}

ppl/src/main/antlr/OpenSearchPPLLexer.g4

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,7 @@ ISBLANK: 'ISBLANK';
480480
// COLLECTION FUNCTIONS
481481
ARRAY: 'ARRAY';
482482
ARRAY_LENGTH: 'ARRAY_LENGTH';
483+
ARRAY_TO_CSV: 'ARRAY_TO_CSV';
483484
MVAPPEND: 'MVAPPEND';
484485
MVJOIN: 'MVJOIN';
485486
MVINDEX: 'MVINDEX';

ppl/src/main/antlr/OpenSearchPPLParser.g4

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,7 @@ geoipFunctionName
12311231
collectionFunctionName
12321232
: ARRAY
12331233
| ARRAY_LENGTH
1234+
| ARRAY_TO_CSV
12341235
| MVAPPEND
12351236
| MVJOIN
12361237
| MVINDEX

0 commit comments

Comments
 (0)