-
Notifications
You must be signed in to change notification settings - Fork 181
Add max/min eval functions #4333
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
831e361
5f7b2ed
e753052
5afd678
5049d20
d116833
c076751
07fc0b6
7858fd5
f776fb1
7203266
0d583f9
a4ab8bd
160042d
cecaf57
43f5480
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| /* | ||
| * Copyright OpenSearch Contributors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package org.opensearch.sql.data.utils; | ||
|
|
||
| import java.util.Comparator; | ||
|
|
||
| /** Comparator for mixed-type values. */ | ||
| public class MixedTypeComparator implements Comparator<Object> { | ||
|
|
||
| public static final MixedTypeComparator INSTANCE = new MixedTypeComparator(); | ||
|
|
||
| private MixedTypeComparator() {} | ||
|
|
||
| @Override | ||
| public int compare(Object a, Object b) { | ||
| boolean aIsNumeric = isNumeric(a); | ||
| boolean bIsNumeric = isNumeric(b); | ||
|
|
||
| // For same types compare directly | ||
| if (aIsNumeric == bIsNumeric) { | ||
| if (aIsNumeric) { | ||
| return Double.compare(((Number) a).doubleValue(), ((Number) b).doubleValue()); | ||
| } else { | ||
| return Integer.compare(a.toString().compareTo(b.toString()), 0); | ||
| } | ||
| } | ||
| // For mixed types, strings are considered larger than numbers (non-numeric values are treated | ||
| // as strings) | ||
| return aIsNumeric ? -1 : 1; | ||
| } | ||
|
|
||
| private static boolean isNumeric(Object obj) { | ||
| return obj instanceof Number; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you confirm whether comparison between "1" and "2" also considered as numerical comparison?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No they will be compared as strings so max("9", "21") will return "9" |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| /* | ||
| * Copyright OpenSearch Contributors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package org.opensearch.sql.expression.function.udf.math; | ||
|
|
||
| import java.util.Arrays; | ||
| 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.rex.RexCall; | ||
| import org.apache.calcite.sql.type.SqlReturnTypeInference; | ||
| import org.apache.calcite.sql.type.SqlTypeName; | ||
| import org.opensearch.sql.data.utils.MixedTypeComparator; | ||
| import org.opensearch.sql.expression.function.ImplementorUDF; | ||
| import org.opensearch.sql.expression.function.UDFOperandMetadata; | ||
|
|
||
| /** | ||
| * MAX(value1, value2, ...) returns the maximum value from the arguments. For mixed types, strings | ||
| * have higher precedence than numbers. | ||
| */ | ||
| public class MaxFunction extends ImplementorUDF { | ||
|
|
||
| public MaxFunction() { | ||
| super(new MaxImplementor(), NullPolicy.ALL); | ||
| } | ||
|
|
||
| @Override | ||
| public SqlReturnTypeInference getReturnTypeInference() { | ||
| return opBinding -> opBinding.getTypeFactory().createSqlType(SqlTypeName.ANY); | ||
| } | ||
|
|
||
| @Override | ||
| public UDFOperandMetadata getOperandMetadata() { | ||
| return null; | ||
| } | ||
|
|
||
| public static class MaxImplementor implements NotNullImplementor { | ||
|
|
||
| @Override | ||
| public Expression implement( | ||
| RexToLixTranslator translator, RexCall call, List<Expression> translatedOperands) { | ||
| return Expressions.call( | ||
| MaxImplementor.class, "max", Expressions.newArrayInit(Object.class, translatedOperands)); | ||
| } | ||
|
|
||
| public static Object max(Object[] args) { | ||
| return findMax(args); | ||
| } | ||
|
|
||
| private static Object findMax(Object[] args) { | ||
| if (args == null) { | ||
| return null; | ||
| } | ||
|
|
||
| return Arrays.stream(args) | ||
| .filter(Objects::nonNull) | ||
| .max(MixedTypeComparator.INSTANCE) | ||
| .orElse(null); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| /* | ||
| * Copyright OpenSearch Contributors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package org.opensearch.sql.expression.function.udf.math; | ||
|
|
||
| import java.util.Arrays; | ||
| 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.rex.RexCall; | ||
| import org.apache.calcite.sql.type.SqlReturnTypeInference; | ||
| import org.apache.calcite.sql.type.SqlTypeName; | ||
| import org.opensearch.sql.data.utils.MixedTypeComparator; | ||
| import org.opensearch.sql.expression.function.ImplementorUDF; | ||
| import org.opensearch.sql.expression.function.UDFOperandMetadata; | ||
|
|
||
| /** | ||
| * MIN(value1, value2, ...) returns the minimum value from the arguments. For mixed types, numbers | ||
| * have higher precedence than strings. | ||
| */ | ||
| public class MinFunction extends ImplementorUDF { | ||
|
|
||
| public MinFunction() { | ||
| super(new MinImplementor(), NullPolicy.ALL); | ||
| } | ||
|
|
||
| @Override | ||
| public SqlReturnTypeInference getReturnTypeInference() { | ||
| return opBinding -> opBinding.getTypeFactory().createSqlType(SqlTypeName.ANY); | ||
| } | ||
|
|
||
| @Override | ||
| public UDFOperandMetadata getOperandMetadata() { | ||
| return null; | ||
| } | ||
|
|
||
| public static class MinImplementor implements NotNullImplementor { | ||
|
|
||
| @Override | ||
| public Expression implement( | ||
| RexToLixTranslator translator, RexCall call, List<Expression> translatedOperands) { | ||
| return Expressions.call( | ||
| MinImplementor.class, "min", Expressions.newArrayInit(Object.class, translatedOperands)); | ||
| } | ||
|
|
||
| public static Object min(Object[] args) { | ||
| return findMin(args); | ||
| } | ||
|
|
||
| private static Object findMin(Object[] args) { | ||
| if (args == null) { | ||
| return null; | ||
| } | ||
|
|
||
| return Arrays.stream(args) | ||
| .filter(Objects::nonNull) | ||
| .min(MixedTypeComparator.INSTANCE) | ||
| .orElse(null); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| /* | ||
| * Copyright OpenSearch Contributors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package org.opensearch.sql.data.utils; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
|
|
||
| import java.math.BigDecimal; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| class MixedTypeComparatorTest { | ||
|
|
||
| private final MixedTypeComparator comparator = MixedTypeComparator.INSTANCE; | ||
|
|
||
| @Test | ||
| public void testNumericComparison() { | ||
| assertEquals(-1, comparator.compare(1, 2)); | ||
| assertEquals(1, comparator.compare(2, 1)); | ||
| assertEquals(0, comparator.compare(5, 5)); | ||
|
|
||
| // Different numeric types | ||
| assertEquals(-1, comparator.compare(1, 2.5)); | ||
| assertEquals(1, comparator.compare(3.14, 2)); | ||
| assertEquals(0, comparator.compare(4, 4.0)); | ||
| assertEquals(-1, comparator.compare(10L, new BigDecimal("20"))); | ||
| } | ||
|
|
||
| @Test | ||
| public void testStringComparison() { | ||
| assertEquals(-1, comparator.compare("apple", "banana")); | ||
| assertEquals(1, comparator.compare("zebra", "apple")); | ||
| assertEquals(0, comparator.compare("test", "test")); | ||
| assertEquals(-1, comparator.compare("ABC", "abc")); // | ||
| assertEquals(1, comparator.compare("hello", "HELLO")); | ||
| } | ||
|
|
||
| @Test | ||
| public void testMixedTypeComparison() { | ||
| assertEquals(-1, comparator.compare(42, "apple")); | ||
| assertEquals(1, comparator.compare("apple", 42)); | ||
| assertEquals(-1, comparator.compare(3.14, "hello")); | ||
| assertEquals(1, comparator.compare("world", 100L)); | ||
| assertEquals(-1, comparator.compare(0, "0")); | ||
| assertEquals(1, comparator.compare("123", 456)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| ====================== | ||
| Statistical Functions | ||
| ====================== | ||
|
|
||
| .. rubric:: Table of contents | ||
|
|
||
| .. contents:: | ||
| :local: | ||
| :depth: 1 | ||
|
|
||
|
|
||
| MAX | ||
| --- | ||
|
|
||
| Description | ||
| >>>>>>>>>>> | ||
|
|
||
| Usage: max(x, y, ...) returns the maximum value from all provided arguments. Strings are treated as greater than numbers, so if provided both strings and numbers, it will return the maximum string value (lexicographically ordered) | ||
|
|
||
| Note: This function is only available in the eval command context and requires Calcite engine to be enabled. | ||
|
|
||
| Argument type: Variable number of INTEGER/LONG/FLOAT/DOUBLE/STRING arguments | ||
|
|
||
| Return type: Type of the selected argument | ||
|
|
||
| Example:: | ||
|
|
||
| os> source=accounts | eval max_val = MAX(age, 30) | fields age, max_val | ||
| fetched rows / total rows = 4/4 | ||
| +-----+---------+ | ||
| | age | max_val | | ||
| |-----+---------| | ||
| | 32 | 32 | | ||
| | 36 | 36 | | ||
| | 28 | 30 | | ||
| | 33 | 33 | | ||
| +-----+---------+ | ||
|
|
||
| os> source=accounts | eval result = MAX(firstname, 'John') | fields firstname, result | ||
| fetched rows / total rows = 4/4 | ||
| +-----------+---------+ | ||
| | firstname | result | | ||
| |-----------+---------| | ||
| | Amber | John | | ||
| | Hattie | John | | ||
| | Nanette | Nanette | | ||
| | Dale | John | | ||
| +-----------+---------+ | ||
|
|
||
| os> source=accounts | eval result = MAX(age, 35, 'John', firstname) | fields age, firstname, result | ||
| fetched rows / total rows = 4/4 | ||
| +-----+-----------+---------+ | ||
| | age | firstname | result | | ||
| |-----+-----------+---------| | ||
| | 32 | Amber | John | | ||
| | 36 | Hattie | John | | ||
| | 28 | Nanette | Nanette | | ||
| | 33 | Dale | John | | ||
| +-----+-----------+---------+ | ||
|
|
||
|
|
||
| MIN | ||
| --- | ||
|
|
||
| Description | ||
| >>>>>>>>>>> | ||
|
|
||
| Usage: min(x, y, ...) returns the minimum value from all provided arguments. Strings are treated as greater than numbers, so if provided both strings and numbers, it will return the minimum numeric value. | ||
|
|
||
| Note: This function is only available in the eval command context and requires Calcite engine to be enabled. | ||
|
|
||
| Argument type: Variable number of INTEGER/LONG/FLOAT/DOUBLE/STRING arguments | ||
|
|
||
| Return type: Type of the selected argument | ||
|
|
||
| Example:: | ||
|
|
||
| os> source=accounts | eval min_val = MIN(age, 30) | fields age, min_val | ||
| fetched rows / total rows = 4/4 | ||
| +-----+---------+ | ||
| | age | min_val | | ||
| |-----+---------| | ||
| | 32 | 30 | | ||
| | 36 | 30 | | ||
| | 28 | 28 | | ||
| | 33 | 30 | | ||
| +-----+---------+ | ||
|
|
||
| os> source=accounts | eval result = MIN(firstname, 'John') | fields firstname, result | ||
| fetched rows / total rows = 4/4 | ||
| +-----------+--------+ | ||
| | firstname | result | | ||
| |-----------+--------| | ||
| | Amber | Amber | | ||
| | Hattie | Hattie | | ||
| | Nanette | John | | ||
| | Dale | Dale | | ||
| +-----------+--------+ | ||
|
|
||
| os> source=accounts | eval result = MIN(age, 35, firstname) | fields age, firstname, result | ||
| fetched rows / total rows = 4/4 | ||
| +-----+-----------+--------+ | ||
| | age | firstname | result | | ||
| |-----+-----------+--------| | ||
| | 32 | Amber | 32 | | ||
| | 36 | Hattie | 35 | | ||
| | 28 | Nanette | 28 | | ||
| | 33 | Dale | 33 | | ||
| +-----+-----------+--------+ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what does this Integer.compare with 0 meaning?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will normalize it so if the string comparison returns a negative it will make it -1 and if it returns a positive it will be 1. Don't think that is necessary, can remove and just leave the string comparison