Skip to content
Merged
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 @@ -336,7 +336,7 @@ public Expression visitCase(Case node, AnalysisContext context) {
}

Expression defaultResult =
(node.getElseClause() == null) ? null : analyze(node.getElseClause(), context);
node.getElseClause().map(elseClause -> analyze(elseClause, context)).orElse(null);
CaseClause caseClause = new CaseClause(whens, defaultResult);

// To make this simple, require all result type same regardless of implicit convert
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ public UnresolvedExpression caseWhen(UnresolvedExpression elseClause, When... wh
*/
public UnresolvedExpression caseWhen(
UnresolvedExpression caseValueExpr, UnresolvedExpression elseClause, When... whenClauses) {
return new Case(caseValueExpr, Arrays.asList(whenClauses), elseClause);
return new Case(caseValueExpr, Arrays.asList(whenClauses), Optional.ofNullable(elseClause));
}

public UnresolvedExpression cast(UnresolvedExpression expr, Literal type) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.Optional;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
Expand All @@ -31,7 +32,7 @@ public class Case extends UnresolvedExpression {
private final List<When> whenClauses;

/** Expression that represents ELSE statement result. */
private final UnresolvedExpression elseClause;
private final Optional<UnresolvedExpression> elseClause;

@Override
public List<? extends Node> getChild() {
Expand All @@ -40,10 +41,7 @@ public List<? extends Node> getChild() {
children.add(caseValue);
}
children.addAll(whenClauses);

if (elseClause != null) {
children.add(elseClause);
}
elseClause.ifPresent(children::add);
return children.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import static org.opensearch.sql.calcite.utils.UserDefinedFunctionUtils.TransferUserDefinedFunction;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
Expand All @@ -31,6 +32,7 @@
import org.opensearch.sql.ast.expression.Alias;
import org.opensearch.sql.ast.expression.And;
import org.opensearch.sql.ast.expression.Between;
import org.opensearch.sql.ast.expression.Case;
import org.opensearch.sql.ast.expression.Cast;
import org.opensearch.sql.ast.expression.Compare;
import org.opensearch.sql.ast.expression.EqualTo;
Expand Down Expand Up @@ -89,7 +91,18 @@ public RexNode visitLiteral(Literal node, CalcitePlanContext context) {
case NULL:
return rexBuilder.makeNullLiteral(typeFactory.createSqlType(SqlTypeName.NULL));
case STRING:
return rexBuilder.makeLiteral(value.toString());
if (value.toString().length() == 1) {
// To align Spark/PostgreSQL, Char(1) is useful, such as cast('1' to boolean) should
// return true
return rexBuilder.makeLiteral(
value.toString(), typeFactory.createSqlType(SqlTypeName.CHAR));
} else {
// Specific the type to VARCHAR and allowCast to true, or the STRING will be optimized to
// CHAR(n)
// which leads to incorrect return type in deriveReturnType of some functions/operators
return rexBuilder.makeLiteral(
value.toString(), typeFactory.createSqlType(SqlTypeName.VARCHAR), true);
}
case INTEGER:
return rexBuilder.makeExactLiteral(new BigDecimal((Integer) value));
case LONG:
Expand Down Expand Up @@ -431,6 +444,19 @@ public RexNode visitCast(Cast node, CalcitePlanContext context) {
return context.rexBuilder.makeCast(nullableType, expr, true, true);
}

@Override
public RexNode visitCase(Case node, CalcitePlanContext context) {
List<RexNode> caseOperands = new ArrayList<>();
for (When when : node.getWhenClauses()) {
caseOperands.add(analyze(when.getCondition(), context));
caseOperands.add(analyze(when.getResult(), context));
}
RexNode elseExpr =
node.getElseClause().map(e -> analyze(e, context)).orElse(context.relBuilder.literal(null));
caseOperands.add(elseExpr);
return context.rexBuilder.makeCall(SqlStdOperatorTable.CASE, caseOperands);
}

/*
* Unsupported Expressions of PPL with Calcite for OpenSearch 3.0.0-beta
*/
Expand Down
46 changes: 46 additions & 0 deletions docs/user/ppl/functions/condition.rst
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,49 @@ Example::
| False | Nanette | Bates |
| False | Dale | Adams |
+--------+-----------+----------+

CASE
------

Description
>>>>>>>>>>>

Usage: case(condition1, expr1, condition2, expr2, ... conditionN, exprN else default) return expr1 if condition1 is true, or return expr2 if condition2 is true, ... if no condition is true, then return the value of ELSE clause. If the ELSE clause is not defined, it returns NULL.

Argument type: all the supported data type, (NOTE : there is no comma before "else")

Return type: any

Example::

os> source=accounts | eval result = case(age > 35, firstname, age < 30, lastname else employer) | fields result, firstname, lastname, age, employer
fetched rows / total rows = 4/4
+--------+-----------+----------+-----+----------+
| result | firstname | lastname | age | employer |
|--------+-----------+----------+-----+----------|
| Pyrami | Amber | Duke | 32 | Pyrami |
| Hattie | Hattie | Bond | 36 | Netagy |
| Bates | Nanette | Bates | 28 | Quility |
| null | Dale | Adams | 33 | null |
+--------+-----------+----------+-----+----------+

os> source=accounts | eval result = case(age > 35, firstname, age < 30, lastname) | fields result, firstname, lastname, age
fetched rows / total rows = 4/4
+--------+-----------+----------+-----+
| result | firstname | lastname | age |
|--------+-----------+----------+-----|
| null | Amber | Duke | 32 |
| Hattie | Hattie | Bond | 36 |
| Bates | Nanette | Bates | 28 |
| null | Dale | Adams | 33 |
+--------+-----------+----------+-----+

os> source=accounts | where true = case(age > 35, false, age < 30, false else true) | fields firstname, lastname, age
fetched rows / total rows = 2/2
+-----------+----------+-----+
| firstname | lastname | age |
|-----------+----------+-----|
| Amber | Duke | 32 |
| Dale | Adams | 33 |
+-----------+----------+-----+

Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.calcite.standalone;

import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_WEBLOGS;
import static org.opensearch.sql.util.MatcherUtils.rows;
import static org.opensearch.sql.util.MatcherUtils.schema;
import static org.opensearch.sql.util.MatcherUtils.verifyDataRows;
import static org.opensearch.sql.util.MatcherUtils.verifySchema;

import java.io.IOException;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import org.opensearch.client.Request;
import org.opensearch.sql.legacy.TestsConstants;

public class CalcitePPLCaseFunctionIT extends CalcitePPLIntegTestCase {
@Override
public void init() throws IOException {
super.init();
loadIndex(Index.WEBLOG);
appendDataForBadResponse();
}

private void appendDataForBadResponse() throws IOException {
Request request1 = new Request("PUT", "/" + TEST_INDEX_WEBLOGS + "/_doc/7?refresh=true");
request1.setJsonEntity(
"{\"host\": \"::1\", \"method\": \"GET\", \"url\": \"/history/apollo/\", \"response\":"
+ " \"301\", \"bytes\": \"6245\"}");
client().performRequest(request1);
Request request2 =
new Request("PUT", "/" + TestsConstants.TEST_INDEX_WEBLOGS + "/_doc/8?refresh=true");
request2.setJsonEntity(
"{\"host\": \"0.0.0.2\", \"method\": \"GET\", \"url\":"
+ " \"/shuttle/missions/sts-73/mission-sts-73.html\", \"response\": \"500\", \"bytes\":"
+ " \"4085\"}");
client().performRequest(request2);
Request request3 =
new Request("PUT", "/" + TestsConstants.TEST_INDEX_WEBLOGS + "/_doc/9?refresh=true");
request3.setJsonEntity(
"{\"host\": \"::3\", \"method\": \"GET\", \"url\": \"/shuttle/countdown/countdown.html\","
+ " \"response\": \"403\", \"bytes\": \"3985\"}");
client().performRequest(request3);
Request request4 =
new Request("PUT", "/" + TestsConstants.TEST_INDEX_WEBLOGS + "/_doc/10?refresh=true");
request4.setJsonEntity(
"{\"host\": \"1.2.3.5\", \"method\": \"GET\", \"url\": \"/history/voyager2/\","
+ " \"response\": null, \"bytes\": \"4321\"}");
client().performRequest(request4);
}

@Test
public void testCaseWhenWithCast() {
JSONObject actual =
executeQuery(
String.format(
"""
source=%s
| eval status =
case(
cast(response as int) >= 200 AND cast(response as int) < 300, "Success",
cast(response as int) >= 300 AND cast(response as int) < 400, "Redirection",
cast(response as int) >= 400 AND cast(response as int) < 500, "Client Error",
cast(response as int) >= 500 AND cast(response as int) < 600, "Server Error"
else concat("Incorrect HTTP status code for", url))
| where status != "Success"
""",
TEST_INDEX_WEBLOGS));
verifySchema(
actual,
schema("host", "ip"),
schema("method", "string"),
schema("url", "string"),
schema("response", "string"),
schema("bytes", "string"),
schema("status", "string"));
verifyDataRows(
actual,
rows("::1", "GET", "6245", "301", "/history/apollo/", "Redirection"),
rows(
"0.0.0.2",
"GET",
"4085",
"500",
"/shuttle/missions/sts-73/mission-sts-73.html",
"Server Error"),
rows("::3", "GET", "3985", "403", "/shuttle/countdown/countdown.html", "Client Error"),
rows(
"1.2.3.5",
"GET",
"4321",
null,
"/history/voyager2/",
"Incorrect HTTP status code for/history/voyager2/"));
}

@Test
public void testCaseWhenNoElse() {
JSONObject actual =
executeQuery(
String.format(
"""
source=%s
| eval status =
case(
cast(response as int) >= 200 AND cast(response as int) < 300, "Success",
cast(response as int) >= 300 AND cast(response as int) < 400, "Redirection",
cast(response as int) >= 400 AND cast(response as int) < 500, "Client Error",
cast(response as int) >= 500 AND cast(response as int) < 600, "Server Error")
| where isnull(status) OR status != "Success"
""",
TEST_INDEX_WEBLOGS));
verifySchema(
actual,
schema("host", "ip"),
schema("method", "string"),
schema("url", "string"),
schema("response", "string"),
schema("bytes", "string"),
schema("status", "string"));
verifyDataRows(
actual,
rows("::1", "GET", "6245", "301", "/history/apollo/", "Redirection"),
rows(
"0.0.0.2",
"GET",
"4085",
"500",
"/shuttle/missions/sts-73/mission-sts-73.html",
"Server Error"),
rows("::3", "GET", "3985", "403", "/shuttle/countdown/countdown.html", "Client Error"),
rows("1.2.3.5", "GET", "4321", null, "/history/voyager2/", null));
}

@Test
public void testCaseWhenWithIn() {
JSONObject actual =
executeQuery(
String.format(
"""
source=%s
| eval status =
case(
response in ('200'), "Success",
response in ('300', '301'), "Redirection",
response in ('400', '403'), "Client Error",
response in ('500', '505'), "Server Error"
else concat("Incorrect HTTP status code for", url))
| where status != "Success"
""",
TEST_INDEX_WEBLOGS));
verifySchema(
actual,
schema("host", "ip"),
schema("method", "string"),
schema("url", "string"),
schema("response", "string"),
schema("bytes", "string"),
schema("status", "string"));
verifyDataRows(
actual,
rows("::1", "GET", "6245", "301", "/history/apollo/", "Redirection"),
rows(
"0.0.0.2",
"GET",
"4085",
"500",
"/shuttle/missions/sts-73/mission-sts-73.html",
"Server Error"),
rows("::3", "GET", "3985", "403", "/shuttle/countdown/countdown.html", "Client Error"),
rows(
"1.2.3.5",
"GET",
"4321",
null,
"/history/voyager2/",
"Incorrect HTTP status code for/history/voyager2/"));
}

@Test
public void testCaseWhenInFilter() {
JSONObject actual =
executeQuery(
String.format(
"""
source=%s
| where not true =
case(
response in ('200'), true,
response in ('300', '301'), false,
response in ('400', '403'), false,
response in ('500', '505'), false
else false)
""",
TEST_INDEX_WEBLOGS));
verifySchema(
actual,
schema("host", "ip"),
schema("method", "string"),
schema("url", "string"),
schema("response", "string"),
schema("bytes", "string"));
verifyDataRows(
actual,
rows("::1", "GET", "6245", "301", "/history/apollo/"),
rows("0.0.0.2", "GET", "4085", "500", "/shuttle/missions/sts-73/mission-sts-73.html"),
rows("::3", "GET", "3985", "403", "/shuttle/countdown/countdown.html"),
rows("1.2.3.5", "GET", "4321", null, "/history/voyager2/"));
}

@Test
public void testCaseWhenInSubquery() {
JSONObject actual =
executeQuery(
String.format(
"""
source=%s
| where response in [
source = %s
| eval new_response = case(
response in ('200'), "201",
response in ('300', '301'), "301",
response in ('400', '403'), "403",
response in ('500', '505'), "500"
else concat("Incorrect HTTP status code for", url))
| fields new_response
]
""",
TEST_INDEX_WEBLOGS, TEST_INDEX_WEBLOGS));
verifySchema(
actual,
schema("host", "ip"),
schema("method", "string"),
schema("url", "string"),
schema("response", "string"),
schema("bytes", "string"));
verifyDataRows(
actual,
rows("::1", "GET", "6245", "301", "/history/apollo/"),
rows("0.0.0.2", "GET", "4085", "500", "/shuttle/missions/sts-73/mission-sts-73.html"),
rows("::3", "GET", "3985", "403", "/shuttle/countdown/countdown.html"));
}
}
Loading
Loading