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 72be016ecc2..7777a2ac034 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 @@ -75,6 +75,7 @@ public enum BuiltinFunctionName { MVAPPEND(FunctionName.of("mvappend")), MVJOIN(FunctionName.of("mvjoin")), MVINDEX(FunctionName.of("mvindex")), + MVZIP(FunctionName.of("mvzip")), SPLIT(FunctionName.of("split")), MVDEDUP(FunctionName.of("mvdedup")), FORALL(FunctionName.of("forall")), diff --git a/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MVZipFunctionImpl.java b/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MVZipFunctionImpl.java new file mode 100644 index 00000000000..7d75ae71941 --- /dev/null +++ b/core/src/main/java/org/opensearch/sql/expression/function/CollectionUDF/MVZipFunctionImpl.java @@ -0,0 +1,128 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.expression.function.CollectionUDF; + +import static org.apache.calcite.sql.type.SqlTypeUtil.createArrayType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +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.RelDataType; +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; + +/** + * MVZip function that combines two multivalue fields pairwise with a delimiter. + * + *

This function zips together two arrays by combining the first value of left with the first + * value of right, the second with the second, and so on, up to the length of the shorter array. + * + *

Behavior: + * + *

+ */ +public class MVZipFunctionImpl extends ImplementorUDF { + + public MVZipFunctionImpl() { + // Use ANY: return null if any argument is null + super(new MVZipImplementor(), NullPolicy.ANY); + } + + @Override + public SqlReturnTypeInference getReturnTypeInference() { + return sqlOperatorBinding -> { + RelDataTypeFactory typeFactory = sqlOperatorBinding.getTypeFactory(); + + // mvzip returns an array of VARCHAR (strings) + RelDataType elementType = typeFactory.createSqlType(SqlTypeName.VARCHAR); + return createArrayType( + typeFactory, typeFactory.createTypeWithNullability(elementType, true), true); + }; + } + + @Override + public UDFOperandMetadata getOperandMetadata() { + // First two arguments must be arrays, optional STRING delimiter + return UDFOperandMetadata.wrap( + (CompositeOperandTypeChecker) + OperandTypes.family(SqlTypeFamily.ARRAY, SqlTypeFamily.ARRAY) + .or( + OperandTypes.family( + SqlTypeFamily.ARRAY, SqlTypeFamily.ARRAY, SqlTypeFamily.CHARACTER))); + } + + public static class MVZipImplementor implements NotNullImplementor { + @Override + public Expression implement( + RexToLixTranslator translator, RexCall call, List translatedOperands) { + // Handle both 2-argument (with default delimiter) and 3-argument cases + if (translatedOperands.size() == 2) { + // mvzip(left, right) - use default delimiter "," + return Expressions.call( + Types.lookupMethod( + MVZipFunctionImpl.class, "mvzip", List.class, List.class, String.class), + translatedOperands.get(0), + translatedOperands.get(1), + Expressions.constant(",")); + } else if (translatedOperands.size() == 3) { + // mvzip(left, right, delimiter) + return Expressions.call( + Types.lookupMethod( + MVZipFunctionImpl.class, "mvzip", List.class, List.class, String.class), + translatedOperands.get(0), + translatedOperands.get(1), + translatedOperands.get(2)); + } else { + throw new IllegalArgumentException( + "mvzip expects 2 or 3 arguments, got " + translatedOperands.size()); + } + } + } + + /** + * Combines two multivalue arrays pairwise with a delimiter. + * + * @param left The left multivalue array + * @param right The right multivalue array + * @param delimiter The delimiter to use for joining values (default: ",") + * @return A list of combined values, empty list if either array is empty, or null if either input + * is null + */ + public static List mvzip(List left, List right, String delimiter) { + if (left == null || right == null) { + return null; + } + + List result = new ArrayList<>(); + int minLength = Math.min(left.size(), right.size()); + + for (int i = 0; i < minLength; i++) { + String combined = + Objects.toString(left.get(i), "") + delimiter + Objects.toString(right.get(i), ""); + result.add(combined); + } + + return result; + } +} 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 ac80cf544ae..68ae5b2067c 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 @@ -47,6 +47,7 @@ import org.opensearch.sql.expression.function.CollectionUDF.FilterFunctionImpl; import org.opensearch.sql.expression.function.CollectionUDF.ForallFunctionImpl; import org.opensearch.sql.expression.function.CollectionUDF.MVAppendFunctionImpl; +import org.opensearch.sql.expression.function.CollectionUDF.MVZipFunctionImpl; import org.opensearch.sql.expression.function.CollectionUDF.MapAppendFunctionImpl; import org.opensearch.sql.expression.function.CollectionUDF.MapRemoveFunctionImpl; import org.opensearch.sql.expression.function.CollectionUDF.ReduceFunctionImpl; @@ -392,6 +393,7 @@ public class PPLBuiltinOperators extends ReflectiveSqlOperatorTable { 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"); + public static final SqlOperator MVZIP = new MVZipFunctionImpl().toUDF("mvzip"); public static final SqlOperator FILTER = new FilterFunctionImpl().toUDF("filter"); public static final SqlOperator TRANSFORM = new TransformFunctionImpl().toUDF("transform"); public static final SqlOperator REDUCE = new ReduceFunctionImpl().toUDF("reduce"); 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 d83dcfaeac1..3cce7e34082 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 @@ -154,6 +154,7 @@ import static org.opensearch.sql.expression.function.BuiltinFunctionName.MVDEDUP; import static org.opensearch.sql.expression.function.BuiltinFunctionName.MVINDEX; import static org.opensearch.sql.expression.function.BuiltinFunctionName.MVJOIN; +import static org.opensearch.sql.expression.function.BuiltinFunctionName.MVZIP; import static org.opensearch.sql.expression.function.BuiltinFunctionName.NOT; import static org.opensearch.sql.expression.function.BuiltinFunctionName.NOTEQUAL; import static org.opensearch.sql.expression.function.BuiltinFunctionName.NOW; @@ -1035,6 +1036,7 @@ void populate() { registerOperator(ARRAY, PPLBuiltinOperators.ARRAY); registerOperator(MVAPPEND, PPLBuiltinOperators.MVAPPEND); registerOperator(MVDEDUP, SqlLibraryOperators.ARRAY_DISTINCT); + registerOperator(MVZIP, PPLBuiltinOperators.MVZIP); registerOperator(MAP_APPEND, PPLBuiltinOperators.MAP_APPEND); registerOperator(MAP_CONCAT, SqlLibraryOperators.MAP_CONCAT); registerOperator(MAP_REMOVE, PPLBuiltinOperators.MAP_REMOVE); diff --git a/docs/user/ppl/functions/collection.md b/docs/user/ppl/functions/collection.md index 3c004a22107..ac2c79e8cda 100644 --- a/docs/user/ppl/functions/collection.md +++ b/docs/user/ppl/functions/collection.md @@ -724,4 +724,109 @@ fetched rows / total rows = 1/1 | [alex,celestino,claudia] | +--------------------------+ ``` - \ No newline at end of file + +## MVZIP + +### Description + +Usage: mvzip(mv_left, mv_right, [delim]) combines the values in two multivalue arrays by pairing corresponding elements and joining them into strings. The delimiter is used to specify a delimiting character to join the two values. This is similar to the Python zip command. + +The values are stitched together combining the first value of mv_left with the first value of mv_right, then the second with the second, and so on. Each pair is concatenated into a string using the delimiter. The function stops at the length of the shorter array. + +The delimiter is optional. When specified, it must be enclosed in quotation marks. The default delimiter is a comma ( , ). + +Returns null if either input is null. Returns an empty array if either input array is empty. + +Argument type: mv_left: ARRAY, mv_right: ARRAY, delim: STRING (optional) +Return type: ARRAY of STRING +Example + +```ppl +source=people +| eval hosts = array('host1', 'host2'), ports = array('80', '443'), nserver = mvzip(hosts, ports, ':') +| fields nserver +| head 1 +``` + +Expected output: + +```text +fetched rows / total rows = 1/1 ++----------------------+ +| nserver | +|----------------------| +| [host1:80,host2:443] | ++----------------------+ +``` + +```ppl +source=people +| eval arr1 = array('a', 'b', 'c'), arr2 = array('x', 'y', 'z'), result = mvzip(arr1, arr2, '|') +| fields result +| head 1 +``` + +Expected output: + +```text +fetched rows / total rows = 1/1 ++---------------+ +| result | +|---------------| +| [a|x,b|y,c|z] | ++---------------+ +``` + +```ppl +source=people +| eval arr1 = array('1', '2', '3'), arr2 = array('a', 'b'), result = mvzip(arr1, arr2, '-') +| fields result +| head 1 +``` + +Expected output: + +```text +fetched rows / total rows = 1/1 ++-----------+ +| result | +|-----------| +| [1-a,2-b] | ++-----------+ +``` + +```ppl +source=people +| eval arr1 = array('a', 'b', 'c'), arr2 = array('x', 'y', 'z'), arr3 = array('1', '2', '3'), result = mvzip(mvzip(arr1, arr2, '-'), arr3, ':') +| fields result +| head 1 +``` + +Expected output: + +```text +fetched rows / total rows = 1/1 ++---------------------+ +| result | +|---------------------| +| [a-x:1,b-y:2,c-z:3] | ++---------------------+ +``` + +```ppl +source=people +| eval arr1 = array('a', 'b'), arr2 = array(), result = mvzip(arr1, arr2) +| fields result +| head 1 +``` + +Expected output: + +```text +fetched rows / total rows = 1/1 ++--------+ +| result | +|--------| +| [] | ++--------+ +``` 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 31556e518b9..8b402fcff6e 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 @@ -490,6 +490,75 @@ public void testMvindexRangeSingleElement() throws IOException { verifyDataRows(actual, rows(List.of(3))); } + @Test + public void testMvzipBasic() throws IOException { + // Basic example from spec: eval nserver=mvzip(hosts,ports) + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval hosts = array('host1', 'host2'), ports = array(80, 443), nserver" + + " = mvzip(hosts, ports) | head 1 | fields nserver", + TEST_INDEX_BANK)); + + verifySchema(actual, schema("nserver", "array")); + verifyDataRows(actual, rows(List.of("host1,80", "host2,443"))); + } + + @Test + public void testMvzipWithCustomDelimiter() throws IOException { + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval arr1 = array('a', 'b', 'c'), arr2 = array('x', 'y', 'z'), result" + + " = mvzip(arr1, arr2, '|') | head 1 | fields result", + TEST_INDEX_BANK)); + + verifySchema(actual, schema("result", "array")); + verifyDataRows(actual, rows(List.of("a|x", "b|y", "c|z"))); + } + + @Test + public void testMvzipNested() throws IOException { + // Example from spec: mvzip(mvzip(field1,field2,"|"),field3,"|") + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval field1 = array('a', 'b'), field2 = array('c', 'd'), field3 =" + + " array('e', 'f'), result = mvzip(mvzip(field1, field2, '|'), field3, '|') |" + + " head 1 | fields result", + TEST_INDEX_BANK)); + + verifySchema(actual, schema("result", "array")); + verifyDataRows(actual, rows(List.of("a|c|e", "b|d|f"))); + } + + @Test + public void testMvzipWithEmptyArray() throws IOException { + // When one array is empty, result should be empty array (not null) + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval result = mvzip(array(), array('a', 'b')) | head 1 | fields" + + " result", + TEST_INDEX_BANK)); + + verifySchema(actual, schema("result", "array")); + verifyDataRows(actual, rows(List.of())); + } + + @Test + public void testMvzipWithBothEmptyArrays() throws IOException { + // When both arrays are empty, result should be empty array (not null) + JSONObject actual = + executeQuery( + String.format( + "source=%s | eval result = mvzip(array(), array()) | head 1 | fields result", + TEST_INDEX_BANK)); + + verifySchema(actual, schema("result", "array")); + verifyDataRows(actual, rows(List.of())); + } + @Test public void testMvdedupWithDuplicates() throws IOException { JSONObject actual = diff --git a/ppl/src/main/antlr/OpenSearchPPLLexer.g4 b/ppl/src/main/antlr/OpenSearchPPLLexer.g4 index 9d3b4fe4029..ca2288e46c4 100644 --- a/ppl/src/main/antlr/OpenSearchPPLLexer.g4 +++ b/ppl/src/main/antlr/OpenSearchPPLLexer.g4 @@ -452,6 +452,7 @@ ARRAY_LENGTH: 'ARRAY_LENGTH'; MVAPPEND: 'MVAPPEND'; MVJOIN: 'MVJOIN'; MVINDEX: 'MVINDEX'; +MVZIP: 'MVZIP'; MVDEDUP: 'MVDEDUP'; SPLIT: 'SPLIT'; FORALL: 'FORALL'; diff --git a/ppl/src/main/antlr/OpenSearchPPLParser.g4 b/ppl/src/main/antlr/OpenSearchPPLParser.g4 index bf77fdb5ffc..e65f510fea2 100644 --- a/ppl/src/main/antlr/OpenSearchPPLParser.g4 +++ b/ppl/src/main/antlr/OpenSearchPPLParser.g4 @@ -1125,6 +1125,7 @@ collectionFunctionName | MVJOIN | MVINDEX | MVDEDUP + | MVZIP | SPLIT | FORALL | EXISTS diff --git a/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLArrayFunctionTest.java b/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLArrayFunctionTest.java index 96529adea24..8a350501adb 100644 --- a/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLArrayFunctionTest.java +++ b/ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLArrayFunctionTest.java @@ -291,6 +291,143 @@ public void testMvdedupPreservesOrder() { verifyPPLToSparkSQL(root, expectedSparkSql); } + @Test + public void testMvzipBasic() { + String ppl = + "source=EMP | eval arr1 = array('a', 'b', 'c'), arr2 = array('x', 'y'), result =" + + " mvzip(arr1, arr2) | head 1 | fields result"; + RelNode root = getRelNode(ppl); + + String expectedLogical = + "LogicalProject(result=[$10])\n" + + " LogicalSort(fetch=[1])\n" + + " LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4]," + + " SAL=[$5], COMM=[$6], DEPTNO=[$7], arr1=[array('a', 'b', 'c')], arr2=[array('x'," + + " 'y')], result=[mvzip(array('a', 'b', 'c'), array('x', 'y'))])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n"; + verifyLogical(root, expectedLogical); + + String expectedResult = "result=[a,x, b,y]\n"; + verifyResult(root, expectedResult); + + String expectedSparkSql = + "SELECT MVZIP(ARRAY('a', 'b', 'c'), ARRAY('x', 'y')) `result`\n" + + "FROM `scott`.`EMP`\n" + + "LIMIT 1"; + verifyPPLToSparkSQL(root, expectedSparkSql); + } + + @Test + public void testMvzipWithCustomDelimiter() { + String ppl = + "source=EMP | eval result = mvzip(array('a', 'b'), array('x', 'y'), '|') | head 1 |" + + " fields result"; + RelNode root = getRelNode(ppl); + + String expectedLogical = + "LogicalProject(result=[$8])\n" + + " LogicalSort(fetch=[1])\n" + + " LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4]," + + " SAL=[$5], COMM=[$6], DEPTNO=[$7], result=[mvzip(array('a', 'b'), array('x', 'y')," + + " '|')])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n"; + verifyLogical(root, expectedLogical); + + String expectedResult = "result=[a|x, b|y]\n"; + verifyResult(root, expectedResult); + + String expectedSparkSql = + "SELECT MVZIP(ARRAY('a', 'b'), ARRAY('x', 'y'), '|') `result`\n" + + "FROM `scott`.`EMP`\n" + + "LIMIT 1"; + verifyPPLToSparkSQL(root, expectedSparkSql); + } + + @Test + public void testMvzipNested() { + String ppl = + "source=EMP | eval field1 = array('a', 'b'), field2 = array('c', 'd'), field3 =" + + " array('e', 'f'), result = mvzip(mvzip(field1, field2, '|'), field3, '|') | head 1" + + " | fields result"; + RelNode root = getRelNode(ppl); + + String expectedLogical = + "LogicalProject(result=[$11])\n" + + " LogicalSort(fetch=[1])\n" + + " LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4]," + + " SAL=[$5], COMM=[$6], DEPTNO=[$7], field1=[array('a', 'b')], field2=[array('c'," + + " 'd')], field3=[array('e', 'f')], result=[mvzip(mvzip(array('a', 'b'), array('c'," + + " 'd'), '|'), array('e', 'f'), '|')])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n"; + verifyLogical(root, expectedLogical); + + String expectedResult = "result=[a|c|e, b|d|f]\n"; + verifyResult(root, expectedResult); + + String expectedSparkSql = + "SELECT MVZIP(MVZIP(ARRAY('a', 'b'), ARRAY('c', 'd'), '|'), ARRAY('e', 'f')," + + " '|') `result`\n" + + "FROM `scott`.`EMP`\n" + + "LIMIT 1"; + verifyPPLToSparkSQL(root, expectedSparkSql); + } + + @Test + public void testMvzipWithEmptyLeftArray() { + String ppl = + "source=EMP | eval result = mvzip(array(), array('a', 'b')) | head 1 | fields result"; + RelNode root = getRelNode(ppl); + + String expectedLogical = + "LogicalProject(result=[$8])\n" + + " LogicalSort(fetch=[1])\n" + + " LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4]," + + " SAL=[$5], COMM=[$6], DEPTNO=[$7], result=[mvzip(array(), array('a', 'b'))])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n"; + verifyLogical(root, expectedLogical); + + String expectedSparkSql = + "SELECT MVZIP(ARRAY(), ARRAY('a', 'b')) `result`\n" + "FROM `scott`.`EMP`\n" + "LIMIT 1"; + verifyPPLToSparkSQL(root, expectedSparkSql); + } + + @Test + public void testMvzipWithEmptyRightArray() { + String ppl = + "source=EMP | eval result = mvzip(array('a', 'b'), array()) | head 1 | fields result"; + RelNode root = getRelNode(ppl); + + String expectedLogical = + "LogicalProject(result=[$8])\n" + + " LogicalSort(fetch=[1])\n" + + " LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4]," + + " SAL=[$5], COMM=[$6], DEPTNO=[$7], result=[mvzip(array('a', 'b'), array())])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n"; + verifyLogical(root, expectedLogical); + + String expectedSparkSql = + "SELECT MVZIP(ARRAY('a', 'b'), ARRAY()) `result`\n" + "FROM `scott`.`EMP`\n" + "LIMIT 1"; + verifyPPLToSparkSQL(root, expectedSparkSql); + } + + @Test + public void testMvzipWithBothEmptyArrays() { + String ppl = "source=EMP | eval result = mvzip(array(), array()) | head 1 | fields result"; + RelNode root = getRelNode(ppl); + + String expectedLogical = + "LogicalProject(result=[$8])\n" + + " LogicalSort(fetch=[1])\n" + + " LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4]," + + " SAL=[$5], COMM=[$6], DEPTNO=[$7], result=[mvzip(array(), array())])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n"; + verifyLogical(root, expectedLogical); + + String expectedSparkSql = + "SELECT MVZIP(ARRAY(), ARRAY()) `result`\n" + "FROM `scott`.`EMP`\n" + "LIMIT 1"; + verifyPPLToSparkSQL(root, expectedSparkSql); + } + @Test public void testSplitWithSemicolonDelimiter() { String ppl = diff --git a/ppl/src/test/java/org/opensearch/sql/ppl/utils/PPLQueryDataAnonymizerTest.java b/ppl/src/test/java/org/opensearch/sql/ppl/utils/PPLQueryDataAnonymizerTest.java index bf7525fcf77..58232ae8cf2 100644 --- a/ppl/src/test/java/org/opensearch/sql/ppl/utils/PPLQueryDataAnonymizerTest.java +++ b/ppl/src/test/java/org/opensearch/sql/ppl/utils/PPLQueryDataAnonymizerTest.java @@ -862,6 +862,16 @@ public void testMvindex() { anonymize("source=t | eval result=mvindex(array(1, 2, 3, 4, 5), 1, 3) | fields result")); } + @Test + public void testMvzip() { + // Test mvzip with custom delimiter + assertEquals( + "source=table | eval identifier=mvzip(array(***,***),array(***,***),***) | fields +" + + " identifier", + anonymize( + "source=t | eval result=mvzip(array('a', 'b'), array('x', 'y'), '|') | fields result")); + } + @Test public void testSplit() { // Test split with delimiter