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 @@ -344,8 +344,8 @@ public enum BuiltinFunctionName {
.put("take", BuiltinFunctionName.TAKE)
.put("percentile", BuiltinFunctionName.PERCENTILE_APPROX)
.put("percentile_approx", BuiltinFunctionName.PERCENTILE_APPROX)
// .put("earliest", BuiltinFunctionName.EARLIEST)
// .put("latest", BuiltinFunctionName.LATEST)
.put("earliest", BuiltinFunctionName.EARLIEST)
.put("latest", BuiltinFunctionName.LATEST)
.put("distinct_count_approx", BuiltinFunctionName.DISTINCT_COUNT_APPROX)
.put("pattern", BuiltinFunctionName.INTERNAL_PATTERN)
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -827,7 +827,8 @@ void populate() {
SqlStdOperatorTable.PLUS,
PPLTypeChecker.family(SqlTypeFamily.NUMERIC, SqlTypeFamily.NUMERIC));
// Replace with a custom CompositeOperandTypeChecker to check both operands as
// SqlStdOperatorTable.ITEM.getOperandTypeChecker() checks only the first operand instead
// SqlStdOperatorTable.ITEM.getOperandTypeChecker() checks only the first
// operand instead
// of all operands.
registerOperator(
INTERNAL_ITEM,
Expand All @@ -841,14 +842,18 @@ void populate() {
XOR,
SqlStdOperatorTable.NOT_EQUALS,
PPLTypeChecker.family(SqlTypeFamily.BOOLEAN, SqlTypeFamily.BOOLEAN));
// SqlStdOperatorTable.CASE.getOperandTypeChecker is null. We manually create a type checker
// for it. The second and third operands are required to be of the same type. If not,
// it will throw an IllegalArgumentException with information Can't find leastRestrictive type
// SqlStdOperatorTable.CASE.getOperandTypeChecker is null. We manually create a
// type checker
// for it. The second and third operands are required to be of the same type. If
// not,
// it will throw an IllegalArgumentException with information Can't find
// leastRestrictive type
registerOperator(
IF,
SqlStdOperatorTable.CASE,
PPLTypeChecker.family(SqlTypeFamily.BOOLEAN, SqlTypeFamily.ANY, SqlTypeFamily.ANY));
// Re-define the type checker for is not null, is present, and is null since their original
// Re-define the type checker for is not null, is present, and is null since
// their original
// type checker ANY isn't compatible with struct types.
registerOperator(
IS_NOT_NULL,
Expand Down Expand Up @@ -901,7 +906,8 @@ void populate() {
(FunctionImp2)
(builder, arg1, arg2) -> builder.makeCall(SqlLibraryOperators.STRCMP, arg2, arg1),
PPLTypeChecker.family(SqlTypeFamily.CHARACTER, SqlTypeFamily.CHARACTER));
// SqlStdOperatorTable.SUBSTRING.getOperandTypeChecker is null. We manually create a type
// SqlStdOperatorTable.SUBSTRING.getOperandTypeChecker is null. We manually
// create a type
// checker for it.
register(
SUBSTRING,
Expand Down Expand Up @@ -1051,6 +1057,21 @@ void registerOperator(BuiltinFunctionName functionName, SqlAggFunction aggFuncti
register(functionName, handler, typeChecker);
}

private static RexNode resolveTimeField(List<RexNode> argList, CalcitePlanContext ctx) {
if (argList.isEmpty()) {
// Try to find @timestamp field
var timestampField =
ctx.relBuilder.peek().getRowType().getField("@timestamp", false, false);
if (timestampField == null) {
throw new IllegalArgumentException(
"Default @timestamp field not found. Please specify a time field explicitly.");
}
return ctx.rexBuilder.makeInputRef(timestampField.getType(), timestampField.getIndex());
} else {
return PlanUtils.derefMapCall(argList.get(0));
}
}

void populate() {
registerOperator(MAX, SqlStdOperatorTable.MAX);
registerOperator(MIN, SqlStdOperatorTable.MIN);
Expand Down Expand Up @@ -1089,6 +1110,24 @@ void populate() {
extractTypeCheckerFromUDF(PPLBuiltinOperators.PERCENTILE_APPROX),
PERCENTILE_APPROX.name(),
false));

register(
EARLIEST,
(distinct, field, argList, ctx) -> {
RexNode timeField = resolveTimeField(argList, ctx);
return ctx.relBuilder.aggregateCall(SqlStdOperatorTable.ARG_MIN, field, timeField);
},
wrapSqlOperandTypeChecker(
SqlStdOperatorTable.ARG_MIN.getOperandTypeChecker(), EARLIEST.name(), false));

register(
LATEST,
(distinct, field, argList, ctx) -> {
RexNode timeField = resolveTimeField(argList, ctx);
return ctx.relBuilder.aggregateCall(SqlStdOperatorTable.ARG_MAX, field, timeField);
},
wrapSqlOperandTypeChecker(
SqlStdOperatorTable.ARG_MAX.getOperandTypeChecker(), LATEST.name(), false));
}
}

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

package org.opensearch.sql.expression.function;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.lang.reflect.Field;
import java.util.Map;
import java.util.Set;
import org.opensearch.sql.expression.function.PPLFuncImpTable.AggHandler;

public abstract class AggFunctionTestBase {

@SuppressWarnings("unchecked")
protected Map<BuiltinFunctionName, org.apache.commons.lang3.tuple.Pair<?, AggHandler>>
getAggFunctionRegistry() {
try {
PPLFuncImpTable funcTable = PPLFuncImpTable.INSTANCE;
Field field = PPLFuncImpTable.class.getDeclaredField("aggFunctionRegistry");
field.setAccessible(true);
return (Map<BuiltinFunctionName, org.apache.commons.lang3.tuple.Pair<?, AggHandler>>)
field.get(funcTable);
} catch (Exception e) {
throw new RuntimeException("Failed to access aggFunctionRegistry", e);
}
}

protected void assertFunctionIsRegistered(BuiltinFunctionName functionName) {
Map<BuiltinFunctionName, org.apache.commons.lang3.tuple.Pair<?, AggHandler>> registry =
getAggFunctionRegistry();
assertTrue(
registry.containsKey(functionName),
functionName.getName().getFunctionName()
+ " function should be registered in aggregate function registry");
assertNotNull(
registry.get(functionName),
functionName.getName().getFunctionName() + " function handler should not be null");
}

protected void assertFunctionsAreRegistered(BuiltinFunctionName... functionNames) {
for (BuiltinFunctionName functionName : functionNames) {
assertFunctionIsRegistered(functionName);
}
}

protected void assertFunctionHandlerTypes(BuiltinFunctionName... functionNames) {
Map<BuiltinFunctionName, org.apache.commons.lang3.tuple.Pair<?, AggHandler>> registry =
getAggFunctionRegistry();
for (BuiltinFunctionName functionName : functionNames) {
org.apache.commons.lang3.tuple.Pair<?, AggHandler> registryEntry = registry.get(functionName);
assertNotNull(
registryEntry, functionName.getName().getFunctionName() + " should be registered");

// Extract the AggHandler from the pair
AggHandler handler = registryEntry.getRight();

assertNotNull(
handler, functionName.getName().getFunctionName() + " handler should not be null");
assertTrue(
handler instanceof AggHandler,
functionName.getName().getFunctionName()
+ " handler should implement AggHandler interface");
}
}

protected void assertRegistryMinimumSize(int expectedMinimumSize) {
Map<BuiltinFunctionName, org.apache.commons.lang3.tuple.Pair<?, AggHandler>> registry =
getAggFunctionRegistry();
assertTrue(
registry.size() >= expectedMinimumSize,
"Registry should contain at least " + expectedMinimumSize + " aggregate functions");
}

protected void assertKnownFunctionsPresent(Set<BuiltinFunctionName> knownFunctions) {
Map<BuiltinFunctionName, org.apache.commons.lang3.tuple.Pair<?, AggHandler>> registry =
getAggFunctionRegistry();
long foundFunctions = registry.keySet().stream().filter(knownFunctions::contains).count();

assertTrue(
foundFunctions >= knownFunctions.size(),
"Should have at least " + knownFunctions.size() + " known aggregate functions registered");
}

protected void assertFunctionNameResolution(
String functionName, BuiltinFunctionName expectedEnum) {
assertTrue(
BuiltinFunctionName.of(functionName).isPresent(),
"Should be able to resolve '" + functionName + "' function name");
assertTrue(
BuiltinFunctionName.of(functionName).get() == expectedEnum,
"Resolved function should match expected enum value");
}

protected void assertFunctionNamesInEnum(BuiltinFunctionName... functionNames) {
Set<BuiltinFunctionName> enumValues = Set.of(BuiltinFunctionName.values());

for (BuiltinFunctionName functionName : functionNames) {
assertTrue(
enumValues.contains(functionName),
functionName.getName().getFunctionName()
+ " should be defined in BuiltinFunctionName enum");
}
}

protected void assertFunctionNameMapping(BuiltinFunctionName functionEnum, String expectedName) {
assertTrue(
functionEnum.getName().getFunctionName().equals(expectedName),
"Function enum should map to expected name: " + expectedName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.expression.function;

import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.util.Set;
import org.junit.jupiter.api.Test;

public class EarliestLatestAggFunctionTest extends AggFunctionTestBase {

@Test
void testEarliestFunctionIsRegistered() {
assertFunctionIsRegistered(BuiltinFunctionName.EARLIEST);
}

@Test
void testLatestFunctionIsRegistered() {
assertFunctionIsRegistered(BuiltinFunctionName.LATEST);
}

@Test
void testBuiltinFunctionNameMapping() {
assertFunctionNameMapping(BuiltinFunctionName.EARLIEST, "earliest");
assertFunctionNameMapping(BuiltinFunctionName.LATEST, "latest");
}

@Test
void testFunctionNameResolution() {
assertFunctionNameResolution("earliest", BuiltinFunctionName.EARLIEST);
assertFunctionNameResolution("latest", BuiltinFunctionName.LATEST);
}

@Test
void testResolveAggWithValidFunctions() {
try {
java.lang.reflect.Method method =
PPLFuncImpTable.class.getDeclaredMethod(
"resolveAgg",
BuiltinFunctionName.class,
boolean.class,
org.apache.calcite.rex.RexNode.class,
java.util.List.class,
org.opensearch.sql.calcite.CalcitePlanContext.class);

assertNotNull(method, "resolveAgg method should exist");
} catch (NoSuchMethodException e) {
throw new RuntimeException("resolveAgg method not found", e);
}
}

@Test
void testFunctionRegistryIntegrity() {
assertFunctionsAreRegistered(BuiltinFunctionName.EARLIEST, BuiltinFunctionName.LATEST);
}

@Test
void testFunctionHandlerTypes() {
assertFunctionHandlerTypes(BuiltinFunctionName.EARLIEST, BuiltinFunctionName.LATEST);
}

@Test
void testFunctionRegistrySize() {
assertRegistryMinimumSize(10);

Set<BuiltinFunctionName> knownFunctions =
Set.of(
BuiltinFunctionName.COUNT,
BuiltinFunctionName.SUM,
BuiltinFunctionName.AVG,
BuiltinFunctionName.MAX,
BuiltinFunctionName.MIN,
BuiltinFunctionName.EARLIEST,
BuiltinFunctionName.LATEST);

assertKnownFunctionsPresent(knownFunctions);
}

@Test
void testEarliestLatestFunctionNamesInEnum() {
assertFunctionNamesInEnum(BuiltinFunctionName.EARLIEST, BuiltinFunctionName.LATEST);
}
}
4 changes: 3 additions & 1 deletion docs/category.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"user/ppl/cmd/rename.rst",
"user/ppl/cmd/search.rst",
"user/ppl/cmd/sort.rst",
"user/ppl/cmd/stats.rst",
"user/ppl/cmd/syntax.rst",
"user/ppl/cmd/trendline.rst",
"user/ppl/cmd/top.rst",
Expand Down Expand Up @@ -56,5 +55,8 @@
"user/dql/aggregations.rst",
"user/dql/complex.rst",
"user/dql/metadata.rst"
],
"ppl_cli_calcite": [
"user/ppl/cmd/stats.rst"
]
}
3 changes: 2 additions & 1 deletion docs/user/dql/metadata.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Example 1: Show All Indices Information
SQL query::

os> SHOW TABLES LIKE '%'
fetched rows / total rows = 15/15
fetched rows / total rows = 16/16
+----------------+-------------+------------------+------------+---------+----------+------------+-----------+---------------------------+----------------+
| TABLE_CAT | TABLE_SCHEM | TABLE_NAME | TABLE_TYPE | REMARKS | TYPE_CAT | TYPE_SCHEM | TYPE_NAME | SELF_REFERENCING_COL_NAME | REF_GENERATION |
|----------------+-------------+------------------+------------+---------+----------+------------+-----------+---------------------------+----------------|
Expand All @@ -44,6 +44,7 @@ SQL query::
| docTestCluster | null | accounts | BASE TABLE | null | null | null | null | null | null |
| docTestCluster | null | apache | BASE TABLE | null | null | null | null | null | null |
| docTestCluster | null | books | BASE TABLE | null | null | null | null | null | null |
| docTestCluster | null | events | BASE TABLE | null | null | null | null | null | null |
| docTestCluster | null | json_test | BASE TABLE | null | null | null | null | null | null |
| docTestCluster | null | nested | BASE TABLE | null | null | null | null | null | null |
| docTestCluster | null | nyc_taxi | BASE TABLE | null | null | null | null | null | null |
Expand Down
Loading
Loading