diff --git a/core/src/main/java/org/opensearch/sql/calcite/utils/BuiltinFunctionUtils.java b/core/src/main/java/org/opensearch/sql/calcite/utils/BuiltinFunctionUtils.java index 20fb8a0208c..3bf5fab7aa3 100644 --- a/core/src/main/java/org/opensearch/sql/calcite/utils/BuiltinFunctionUtils.java +++ b/core/src/main/java/org/opensearch/sql/calcite/utils/BuiltinFunctionUtils.java @@ -5,30 +5,17 @@ package org.opensearch.sql.calcite.utils; -import java.math.BigDecimal; -import java.util.Locale; -import org.apache.calcite.sql.SqlOperator; -import org.apache.calcite.sql.fun.SqlLibraryOperators; -import org.apache.calcite.sql.fun.SqlStdOperatorTable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Locale; - -import org.apache.calcite.linq4j.tree.Types; import org.apache.calcite.rex.RexNode; -import org.apache.calcite.schema.ScalarFunction; -import org.apache.calcite.schema.impl.ScalarFunctionImpl; -import org.apache.calcite.sql.SqlIdentifier; -import org.apache.calcite.sql.SqlKind; import org.apache.calcite.sql.SqlOperator; import org.apache.calcite.sql.fun.SqlLibraryOperators; import org.apache.calcite.sql.fun.SqlStdOperatorTable; import org.apache.calcite.sql.fun.SqlTrimFunction; -import org.apache.calcite.sql.parser.SqlParserPos; -import org.apache.calcite.sql.type.ReturnTypes; -import org.apache.calcite.sql.validate.SqlUserDefinedFunction; import org.opensearch.sql.calcite.CalcitePlanContext; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; public interface BuiltinFunctionUtils { static SqlOperator translate(String op) { @@ -62,7 +49,7 @@ static SqlOperator translate(String op) { return SqlStdOperatorTable.MULTIPLY; case "/": return SqlStdOperatorTable.DIVIDE; - // Built-in String Functions + // Built-in String Functions case "CONCAT": return SqlLibraryOperators.CONCAT_FUNCTION; case "CONCAT_WS": @@ -99,11 +86,55 @@ static SqlOperator translate(String op) { // Built-in Math Functions case "ABS": return SqlStdOperatorTable.ABS; + case "ACOS": + return SqlStdOperatorTable.ACOS; + case "ASIN": + return SqlStdOperatorTable.ASIN; case "ATAN", "ATAN2": return SqlStdOperatorTable.ATAN2; + case "CEILING": + return SqlStdOperatorTable.CEIL; + case "CONV": + return SqlStdOperatorTable.CONVERT; + case "COS": + return SqlStdOperatorTable.COS; + case "COT": + return SqlStdOperatorTable.COT; + case "DEGREES": + return SqlStdOperatorTable.DEGREES; + case "EXP": + return SqlStdOperatorTable.EXP; + case "FLOOR": + return SqlStdOperatorTable.FLOOR; + case "LN": + return SqlStdOperatorTable.LN; case "LOG": return SqlLibraryOperators.LOG; - // Built-in Date Functions + case "LOG2": + return SqlLibraryOperators.LOG2; + case "LOG10": + return SqlStdOperatorTable.LOG10; + case "MOD": + return SqlStdOperatorTable.MOD; + case "PI": + return SqlStdOperatorTable.PI; + case "POW", "POWER": + return SqlStdOperatorTable.POWER; + case "RADIANS": + return SqlStdOperatorTable.RADIANS; + case "RAND": + return SqlStdOperatorTable.RAND; + case "ROUND": + return SqlStdOperatorTable.ROUND; + case "SIGN": + return SqlStdOperatorTable.SIGN; + case "SIN": + return SqlStdOperatorTable.SIN; + case "SQRT": + return SqlStdOperatorTable.SQRT; + case "CBRT": + return SqlStdOperatorTable.CBRT; + // Built-in Date Functions case "CURRENT_TIMESTAMP": return SqlStdOperatorTable.CURRENT_TIMESTAMP; case "CURRENT_DATE": diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/CalcitePPLBasicIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/CalcitePPLBasicIT.java index 4d19a73e8e3..0f5514d385a 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/CalcitePPLBasicIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/CalcitePPLBasicIT.java @@ -5,13 +5,20 @@ package org.opensearch.sql.calcite; -import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_BANK; - -import java.io.IOException; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import org.junit.Ignore; import org.junit.jupiter.api.Test; import org.opensearch.client.Request; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.List; + +import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_BANK; + + public class CalcitePPLBasicIT extends CalcitePPLIntegTestCase { @Override @@ -23,7 +30,9 @@ public void init() throws IOException { Request request2 = new Request("PUT", "/test/_doc/2?refresh=true"); request2.setJsonEntity("{\"name\": \"world\", \"age\": 30}"); client().performRequest(request2); - + Request request3 = new Request("PUT", "/people/_doc/2?refresh=true"); + request3.setJsonEntity("{\"name\": \"DummyEntityForMathVerification\", \"age\": 24}"); + client().performRequest(request3); loadIndex(Index.BANK); } @@ -1065,6 +1074,226 @@ public void testUpper() { actual); } + private static JsonArray parseAndGetFirstDataRow(String executionResult) { + JsonObject sqrtResJson = JsonParser.parseString(executionResult).getAsJsonObject(); + JsonArray dataRows = sqrtResJson.getAsJsonArray("datarows"); + return dataRows.get(0).getAsJsonArray(); + } + + private void testMathPPL(String query, List expectedValues){ + String execResult = execute(query); + JsonArray dataRow = parseAndGetFirstDataRow(execResult); + assertEquals(expectedValues.size(), dataRow.size()); + for (int i = 0; i < expectedValues.size(); i++){ + Number expected = expectedValues.get(i); + Number actual = dataRow.get(i).getAsNumber(); + if (expected instanceof BigDecimal) { + assertEquals(expected, actual); + } + else if (expected instanceof Double || expected instanceof Float) { + assertDoubleUlpEquals(expected.doubleValue(), actual.doubleValue(), 8); + } else if (expected instanceof Long || expected instanceof Integer) { + assertEquals(expected.longValue(), actual.longValue()); + } else { + fail("Unsupported number type: " + expected.getClass().getName()); + } + } + } + + @Test + public void testAbs() { + String absPpl = "source=people | eval `ABS(-1)` = ABS(-1) | fields `ABS(-1)`"; + List expected = List.of(1); + testMathPPL(absPpl, expected); + } + + @Test + public void testAcos() { + String acosPpl = "source=people | eval `ACOS(0)` = ACOS(0) | fields `ACOS(0)`"; + List expected = List.of(Math.PI / 2); + testMathPPL(acosPpl, expected); + } + + @Test + public void testAsin() { + String asinPpl = "source=people | eval `ASIN(0)` = ASIN(0) | fields `ASIN(0)`"; + List expected = List.of(0.0); + testMathPPL(asinPpl, expected); + } + + @Test + public void testAtan() { + // TODO: Error while preparing plan [LogicalProject(ATAN(2)=[ATAN(2)], ATAN(2, 3)=[ATAN(2, 3)]) + // ATAN defined in OpenSearch accepts single and double arguments, while that defined in SQL standard library accepts only single argument. + testMathPPL("source=people | eval `ATAN(2)` = ATAN(2), `ATAN(2, 3)` = ATAN(2, 3) | fields `ATAN(2)`, `ATAN(2, 3)`", List.of(Math.atan(2), Math.atan2(2, 3))); + } + + @Test + public void testAtan2() { + testMathPPL("source=people | eval `ATAN2(2, 3)` = ATAN2(2, 3) | fields `ATAN2(2, 3)`", List.of(Math.atan2(2, 3))); + } + + @Test + public void testCeiling() { + testMathPPL( + "source=people | eval `CEILING(0)` = CEILING(0), `CEILING(50.00005)` = CEILING(50.00005), `CEILING(-50.00005)` = CEILING(-50.00005) | fields `CEILING(0)`, `CEILING(50.00005)`, `CEILING(-50.00005)`", + List.of(Math.ceil(0.0), Math.ceil(50.00005), Math.ceil(-50.00005))); + testMathPPL( + "source=people | eval `CEILING(3147483647.12345)` = CEILING(3147483647.12345), `CEILING(113147483647.12345)` = CEILING(113147483647.12345), `CEILING(3147483647.00001)` = CEILING(3147483647.00001) | fields `CEILING(3147483647.12345)`, `CEILING(113147483647.12345)`, `CEILING(3147483647.00001)`", + List.of(Math.ceil(3147483647.12345), Math.ceil(113147483647.12345), Math.ceil(3147483647.00001))); + } + + @Test + public void testConv() { + // TODO: Error while preparing plan [LogicalProject(CONV('12', 10, 16)=[CONVERT('12', 10, 16)], CONV('2C', 16, 10)=[CONVERT('2C', 16, 10)], CONV(12, 10, 2)=[CONVERT(12, 10, 2)], CONV(1111, 2, 10)=[CONVERT(1111, 2, 10)]) + // OpenSearchTableScan(table=[[OpenSearch, people]]) + String convPpl = "source=people | eval `CONV('12', 10, 16)` = CONV('12', 10, 16), `CONV('2C', 16, 10)` = CONV('2C', 16, 10), `CONV(12, 10, 2)` = CONV(12, 10, 2), `CONV(1111, 2, 10)` = CONV(1111, 2, 10) | fields `CONV('12', 10, 16)`, `CONV('2C', 16, 10)`, `CONV(12, 10, 2)`, `CONV(1111, 2, 10)`"; + String execResult = execute(convPpl); + JsonArray dataRow = parseAndGetFirstDataRow(execResult); + assertEquals(4, dataRow.size()); + assertEquals("c", dataRow.get(0).getAsString()); + assertEquals("44", dataRow.get(1).getAsString()); + assertEquals("1100", dataRow.get(2).getAsString()); + assertEquals("15", dataRow.get(3).getAsString()); + } + + @Test + public void testCos() { + testMathPPL("source=people | eval `COS(0)` = COS(0) | fields `COS(0)`", List.of(1.0)); + } + + @Test + public void testCot() { + testMathPPL("source=people | eval `COT(1)` = COT(1) | fields `COT(1)`", List.of(1.0 / Math.tan(1))); + } + + @Test + public void testCrc32() { + //TODO: No corresponding built-in implementation + testMathPPL("source=people | eval `CRC32('MySQL')` = CRC32('MySQL') | fields `CRC32('MySQL')`", List.of(3259397556L)); + } + + @Test + public void testDegrees() { + testMathPPL("source=people | eval `DEGREES(1.57)` = DEGREES(1.57) | fields `DEGREES(1.57)`", List.of(Math.toDegrees(1.57))); + } + + @Test + public void testEuler() { + //TODO: No corresponding built-in implementation + testMathPPL("source=people | eval `E()` = E() | fields `E()`", List.of(Math.E)); + } + @Test + public void testExp() { + testMathPPL("source=people | eval `EXP(2)` = EXP(2) | fields `EXP(2)`", List.of(Math.exp(2))); + } + + @Test + public void testFloor() { + testMathPPL( + "source=people | eval `FLOOR(0)` = FLOOR(0), `FLOOR(50.00005)` = FLOOR(50.00005), `FLOOR(-50.00005)` = FLOOR(-50.00005) | fields `FLOOR(0)`, `FLOOR(50.00005)`, `FLOOR(-50.00005)`", + List.of(Math.floor(0.0), Math.floor(50.00005), Math.floor(-50.00005))); + testMathPPL( + "source=people | eval `FLOOR(3147483647.12345)` = FLOOR(3147483647.12345), `FLOOR(113147483647.12345)` = FLOOR(113147483647.12345), `FLOOR(3147483647.00001)` = FLOOR(3147483647.00001) | fields `FLOOR(3147483647.12345)`, `FLOOR(113147483647.12345)`, `FLOOR(3147483647.00001)`", + List.of(Math.floor(3147483647.12345), Math.floor(113147483647.12345), Math.floor(3147483647.00001))); + testMathPPL( + "source=people | eval `FLOOR(282474973688888.022)` = FLOOR(282474973688888.022), `FLOOR(9223372036854775807.022)` = FLOOR(9223372036854775807.022), `FLOOR(9223372036854775807.0000001)` = FLOOR(9223372036854775807.0000001) | fields `FLOOR(282474973688888.022)`, `FLOOR(9223372036854775807.022)`, `FLOOR(9223372036854775807.0000001)`", + List.of(Math.floor(282474973688888.022), Math.floor(9223372036854775807.022), Math.floor(9223372036854775807.0000001))); + } + + @Test + public void testLn() { + testMathPPL("source=people | eval `LN(2)` = LN(2) | fields `LN(2)`", List.of(Math.log(2))); + } + + @Test + public void testLog() { + // TODO: No built-in function for 2-operand log + testMathPPL("source=people | eval `LOG(2)` = LOG(2), `LOG(2, 8)` = LOG(2, 8) | fields `LOG(2)`, `LOG(2, 8)`", List.of(Math.log(2), Math.log(8) / Math.log(2))); + } + + @Test + public void testLog2() { + testMathPPL("source=people | eval `LOG2(8)` = LOG2(8) | fields `LOG2(8)`", List.of(Math.log(8) / Math.log(2))); + } + + @Test + public void testLog10() { + testMathPPL("source=people | eval `LOG10(100)` = LOG10(100) | fields `LOG10(100)`", List.of(Math.log10(100))); + } + + @Test + public void testMod() { + // TODO: There is a difference between MOD in OpenSearch and SQL standard library + // For MOD in Calcite, MOD(3.1, 2) = 1 + testMathPPL( + "source=people | eval `MOD(3, 2)` = MOD(3, 2), `MOD(3.1, 2)` = MOD(3.1, 2) | fields `MOD(3, 2)`, `MOD(3.1, 2)`", + List.of(1, 1.1)); + } + + @Test + public void testPi() { + testMathPPL("source=people | eval `PI()` = PI() | fields `PI()`", List.of(Math.PI)); + } + + @Test + public void testPowAndPower() { + testMathPPL( + "source=people | eval `POW(3, 2)` = POW(3, 2), `POW(-3, 2)` = POW(-3, 2), `POW(3, -2)` = POW(3, -2) | fields `POW(3, 2)`, `POW(-3, 2)`, `POW(3, -2)`", + List.of(Math.pow(3, 2), Math.pow(-3, 2), Math.pow(3, -2))); + testMathPPL( + "source=people | eval `POWER(3, 2)` = POWER(3, 2), `POWER(-3, 2)` = POWER(-3, 2), `POWER(3, -2)` = POWER(3, -2) | fields `POWER(3, 2)`, `POWER(-3, 2)`, `POWER(3, -2)`", + List.of(Math.pow(3, 2), Math.pow(-3, 2), Math.pow(3, -2))); + } + + @Test + public void testRadians() { + testMathPPL("source=people | eval `RADIANS(90)` = RADIANS(90) | fields `RADIANS(90)`", List.of(Math.toRadians(90))); + } + + @Test + public void testRand() { + String randPpl = "source=people | eval `RAND(3)` = RAND(3) | fields `RAND(3)`"; + String execResult1 = execute(randPpl); + String execResult2 = execute(randPpl); + assertEquals(execResult1, execResult2); + double val = parseAndGetFirstDataRow(execResult1).get(0).getAsDouble(); + assertTrue(val >= 0 && val <= 1); + } + + @Test + public void testRound() { + testMathPPL( + "source=people | eval `ROUND(12.34)` = ROUND(12.34), `ROUND(12.34, 1)` = ROUND(12.34, 1), `ROUND(12.34, -1)` = ROUND(12.34, -1), `ROUND(12, 1)` = ROUND(12, 1) | fields `ROUND(12.34)`, `ROUND(12.34, 1)`, `ROUND(12.34, -1)`, `ROUND(12, 1)`", + List.of(Math.round(12.34), Math.round(12.34 * 10) / 10.0, Math.round(12.34 / 10) * 10.0, Math.round(12.0 * 10) / 10.0) + ); + } + + @Test + public void testSign() { + testMathPPL( + "source=people | eval `SIGN(1)` = SIGN(1), `SIGN(0)` = SIGN(0), `SIGN(-1.1)` = SIGN(-1.1) | fields `SIGN(1)`, `SIGN(0)`, `SIGN(-1.1)`", + List.of(1, 0, -1) + ); + } + + @Test + public void testSin() { + testMathPPL("source=people | eval `SIN(0)` = SIN(0) | fields `SIN(0)`", List.of(Math.sin(0.0))); + } + + @Test + public void testSqrt() { + testMathPPL("source=people | eval `SQRT(4)` = SQRT(4), `SQRT(4.41)` = SQRT(4.41) | fields `SQRT(4)`, `SQRT(4.41)`", List.of(Math.sqrt(4), Math.sqrt(4.41))); + } + + @Test + public void testCbrt() { + testMathPPL( + "source=people | eval `CBRT(8)` = CBRT(8), `CBRT(9.261)` = CBRT(9.261), `CBRT(-27)` = CBRT(-27) | fields `CBRT(8)`, `CBRT(9.261)`, `CBRT(-27)`", + List.of(Math.cbrt(8), Math.cbrt(9.261), Math.cbrt(-27)) + ); + } }