Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* 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<Expression> 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<Object> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Object> array = Arrays.asList("GET", "READ", "WRITE");
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ",");
assertEquals("GET,READ,WRITE", result);
}

@Test
void testArrayToCsvWithCustomDelimiter() {
List<Object> array = Arrays.asList("GET", "READ", "WRITE");
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ", ");
assertEquals("GET, READ, WRITE", result);
}

@Test
void testArrayToCsvWithPipeDelimiter() {
List<Object> array = Arrays.asList("GET", "READ", "WRITE");
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, " | ");
assertEquals("GET | READ | WRITE", result);
}

@Test
void testArrayToCsvWithEmptyArray() {
List<Object> array = Collections.emptyList();
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ",");
assertEquals("", result);
}

@Test
void testArrayToCsvWithNullArray() {
String result = ArrayToCsvFunctionImpl.arrayToCsv(null, ",");
assertNull(result);
}

@Test
void testArrayToCsvWithNullElements() {
List<Object> array = Arrays.asList("GET", null, "WRITE");
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ",");
assertEquals("GET,,WRITE", result);
}

@Test
void testArrayToCsvWithSingleElement() {
List<Object> array = Arrays.asList("GET");
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ",");
assertEquals("GET", result);
}

@Test
void testArrayToCsvWithNumbers() {
List<Object> array = Arrays.asList(1, 2, 3);
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ",");
assertEquals("1,2,3", result);
}

@Test
void testArrayToCsvWithMixedTypes() {
List<Object> array = Arrays.asList("GET", 123, true);
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ",");
assertEquals("GET,123,true", result);
}

@Test
void testArrayToCsvWithNullDelimiter() {
List<Object> array = Arrays.asList("a", "b", "c");
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, null);
assertEquals("a,b,c", result);
}

@Test
void testArrayToCsvWithNullDelimiterAndSingleElement() {
List<Object> array = Arrays.asList("GET");
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, null);
assertEquals("GET", result);
}

@Test
void testArrayToCsvWithNullDelimiterAndEmptyArray() {
List<Object> array = Collections.emptyList();
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, null);
assertEquals("", result);
}

@Test
void testArrayToCsvWithMultipleNullElements() {
List<Object> array = Arrays.asList("a", null, null, "b");
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ",");
assertEquals("a,,,b", result);
}

@Test
void testArrayToCsvWithLeadingNullElement() {
List<Object> array = Arrays.asList(null, "a", "b");
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ",");
assertEquals(",a,b", result);
}

@Test
void testArrayToCsvWithTrailingNullElement() {
List<Object> array = Arrays.asList("a", "b", null);
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ",");
assertEquals("a,b,", result);
}

@Test
void testArrayToCsvWithAllNullElements() {
List<Object> array = Arrays.asList(null, null, null);
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, ",");
assertEquals(",,", result);
}

@Test
void testArrayToCsvWithNullElementsAndCustomDelimiter() {
List<Object> array = Arrays.asList("GET", null, "WRITE");
String result = ArrayToCsvFunctionImpl.arrayToCsv(array, " | ");
assertEquals("GET | | WRITE", result);
}
}
1 change: 1 addition & 0 deletions ppl/src/main/antlr/OpenSearchPPLLexer.g4
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions ppl/src/main/antlr/OpenSearchPPLParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,7 @@ geoipFunctionName
collectionFunctionName
: ARRAY
| ARRAY_LENGTH
| ARRAY_TO_CSV
| MVAPPEND
| MVJOIN
| MVINDEX
Expand Down