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 @@ -81,6 +81,7 @@
import org.opensearch.sql.ast.tree.ML;
import org.opensearch.sql.ast.tree.Multisearch;
import org.opensearch.sql.ast.tree.MvCombine;
import org.opensearch.sql.ast.tree.NoMv;
import org.opensearch.sql.ast.tree.Paginate;
import org.opensearch.sql.ast.tree.Parse;
import org.opensearch.sql.ast.tree.Patterns;
Expand Down Expand Up @@ -546,6 +547,11 @@ public LogicalPlan visitMvCombine(MvCombine node, AnalysisContext context) {
throw getOnlyForCalciteException("mvcombine");
}

@Override
public LogicalPlan visitNoMv(NoMv node, AnalysisContext context) {
throw getOnlyForCalciteException("nomv");
}

/** Build {@link ParseExpression} to context and skip to child nodes. */
@Override
public LogicalPlan visitParse(Parse node, AnalysisContext context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
import org.opensearch.sql.ast.tree.ML;
import org.opensearch.sql.ast.tree.Multisearch;
import org.opensearch.sql.ast.tree.MvCombine;
import org.opensearch.sql.ast.tree.NoMv;
import org.opensearch.sql.ast.tree.Paginate;
import org.opensearch.sql.ast.tree.Parse;
import org.opensearch.sql.ast.tree.Patterns;
Expand Down Expand Up @@ -475,4 +476,8 @@ public T visitAddColTotals(AddColTotals node, C context) {
public T visitMvCombine(MvCombine node, C context) {
return visitChildren(node, context);
}

public T visitNoMv(NoMv node, C context) {
return visitChildren(node, context);
}
}
79 changes: 79 additions & 0 deletions core/src/main/java/org/opensearch/sql/ast/tree/NoMv.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.ast.tree;

import com.google.common.collect.ImmutableList;
import java.util.List;
import javax.annotation.Nullable;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import org.opensearch.sql.ast.AbstractNodeVisitor;
import org.opensearch.sql.ast.expression.DataType;
import org.opensearch.sql.ast.expression.Field;
import org.opensearch.sql.ast.expression.Function;
import org.opensearch.sql.ast.expression.Let;
import org.opensearch.sql.ast.expression.Literal;

/**
* AST node for the NOMV command. Converts multi-value fields to single-value fields by joining
* array elements with newline delimiter.
*/
@Getter
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = false)
public class NoMv extends UnresolvedPlan {

private final Field field;
@Nullable private UnresolvedPlan child;

public NoMv(Field field) {
this.field = field;
}

public NoMv attach(UnresolvedPlan child) {
this.child = child;
return this;
}

@Override
public List<UnresolvedPlan> getChild() {
return child == null ? ImmutableList.of() : ImmutableList.of(child);
}

@Override
public <T, C> T accept(AbstractNodeVisitor<T, C> nodeVisitor, C context) {
return nodeVisitor.visitNoMv(this, context);
}

/**
* Rewrites the nomv command as an eval command using mvjoin function with null filtering. nomv
* <field> is rewritten to: eval <field> = coalesce(mvjoin(array_compact(<field>), "\n"), "")
*
* <p>The array_compact removes null elements from the array, and coalesce ensures empty arrays
* return empty string instead of null.
*
* @return an Eval node representing the equivalent mvjoin operation with null filtering
*/
public UnresolvedPlan rewriteAsEval() {
Function arrayCompactFunc = new Function("array_compact", ImmutableList.of(field));

Function mvjoinFunc =
new Function(
"mvjoin", ImmutableList.of(arrayCompactFunc, new Literal("\n", DataType.STRING)));

Function coalesceFunc =
new Function("coalesce", ImmutableList.of(mvjoinFunc, new Literal("", DataType.STRING)));

Let letExpr = new Let(field, coalesceFunc);

Eval eval = new Eval(ImmutableList.of(letExpr));
if (this.child != null) {
eval.attach(this.child);
}
return eval;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
import org.opensearch.sql.ast.tree.ML;
import org.opensearch.sql.ast.tree.Multisearch;
import org.opensearch.sql.ast.tree.MvCombine;
import org.opensearch.sql.ast.tree.NoMv;
import org.opensearch.sql.ast.tree.Paginate;
import org.opensearch.sql.ast.tree.Parse;
import org.opensearch.sql.ast.tree.Patterns;
Expand Down Expand Up @@ -3290,6 +3291,26 @@ private void restoreColumnOrderAfterArrayAgg(
relBuilder.project(projections, projectionNames, /* force= */ true);
}

/**
* Visits a NoMv (no multivalue) node by rewriting it as an Eval node.
*
* <p>The NoMv command converts multivalue (array) fields to single-value strings by joining array
* elements with newline delimiters. Internally, NoMv rewrites itself to an Eval node containing a
* mvjoin function call: {@code eval field = mvjoin(field, "\n")}.
*
* <p>The explicit cast to Eval is safe because {@link NoMv#rewriteAsEval()} always returns a
* newly constructed Eval instance and never returns null or other types.
*
* @param node the NoMv node to visit
* @param context the Calcite plan context containing schema and optimization information
* @return the RelNode resulting from visiting the rewritten Eval node
* @see NoMv#rewriteAsEval()
*/
@Override
public RelNode visitNoMv(NoMv node, CalcitePlanContext context) {
return visitEval((Eval) node.rewriteAsEval(), context);
}

@Override
public RelNode visitValues(Values values, CalcitePlanContext context) {
if (values.getValues() == null || values.getValues().isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public enum BuiltinFunctionName {
ARRAY(FunctionName.of("array")),
ARRAY_LENGTH(FunctionName.of("array_length")),
ARRAY_SLICE(FunctionName.of("array_slice"), true),
ARRAY_COMPACT(FunctionName.of("array_compact")),
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
Expand Up @@ -35,7 +35,7 @@
*/
public class ArrayFunctionImpl extends ImplementorUDF {
public ArrayFunctionImpl() {
super(new ArrayImplementor(), NullPolicy.ANY);
super(new ArrayImplementor(), NullPolicy.NONE);
}

/**
Expand Down Expand Up @@ -81,7 +81,8 @@ public Expression implement(

/**
* The asList will generate the List<Object>. We need to convert internally, otherwise, the
* calcite will directly cast like DOUBLE -> INTEGER, which throw error
* calcite will directly cast like DOUBLE -> INTEGER, which throw error. Null elements are
* preserved in the array.
*/
public static Object internalCast(Object... args) {
List<Object> originalList = (List<Object>) args[0];
Expand All @@ -93,7 +94,9 @@ public static Object internalCast(Object... args) {
originalList.stream()
.map(
num -> {
if (num instanceof BigDecimal) {
if (num == null) {
return null;
} else if (num instanceof BigDecimal) {
return (BigDecimal) num;
} else {
return BigDecimal.valueOf(((Number) num).doubleValue());
Expand All @@ -104,17 +107,20 @@ public static Object internalCast(Object... args) {
case DOUBLE:
result =
originalList.stream()
.map(i -> (Object) ((Number) i).doubleValue())
.map(i -> i == null ? null : (Object) ((Number) i).doubleValue())
.collect(Collectors.toList());
break;
case FLOAT:
result =
originalList.stream()
.map(i -> (Object) ((Number) i).floatValue())
.map(i -> i == null ? null : (Object) ((Number) i).floatValue())
.collect(Collectors.toList());
break;
case VARCHAR, CHAR:
result = originalList.stream().map(i -> (Object) i.toString()).collect(Collectors.toList());
result =
originalList.stream()
.map(i -> i == null ? null : (Object) i.toString())
.collect(Collectors.toList());
break;
default:
result = originalList;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import static org.opensearch.sql.expression.function.BuiltinFunctionName.ADDTIME;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.AND;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.ARRAY;
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.ASCII;
Expand Down Expand Up @@ -990,12 +991,7 @@ void populate() {
PPLTypeChecker.family(SqlTypeFamily.ANY));

// Register MVJOIN to use Calcite's ARRAY_JOIN
register(
MVJOIN,
(FunctionImp2)
(builder, array, delimiter) ->
builder.makeCall(SqlLibraryOperators.ARRAY_JOIN, array, delimiter),
PPLTypeChecker.family(SqlTypeFamily.ARRAY, SqlTypeFamily.CHARACTER));
registerOperator(MVJOIN, SqlLibraryOperators.ARRAY_JOIN);

// Register SPLIT with custom logic for empty delimiter
// Case 1: Delimiter is not empty string, use SPLIT
Expand Down Expand Up @@ -1048,6 +1044,7 @@ void populate() {
registerOperator(MAP_REMOVE, PPLBuiltinOperators.MAP_REMOVE);
registerOperator(ARRAY_LENGTH, SqlLibraryOperators.ARRAY_LENGTH);
registerOperator(ARRAY_SLICE, SqlLibraryOperators.ARRAY_SLICE);
registerOperator(ARRAY_COMPACT, SqlLibraryOperators.ARRAY_COMPACT);
registerOperator(FORALL, PPLBuiltinOperators.FORALL);
registerOperator(EXISTS, PPLBuiltinOperators.EXISTS);
registerOperator(FILTER, PPLBuiltinOperators.FILTER);
Expand Down
Loading
Loading