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 @@ -32,6 +32,7 @@
import org.opensearch.sql.data.model.ExprTupleValue;
import org.opensearch.sql.data.model.ExprValue;
import org.opensearch.sql.data.model.ExprValueUtils;
import org.opensearch.sql.data.type.ExprCoreType;
import org.opensearch.sql.data.type.ExprType;
import org.opensearch.sql.executor.ExecutionContext;
import org.opensearch.sql.executor.ExecutionEngine;
Expand Down Expand Up @@ -123,7 +124,7 @@ public void onResponse(Iterable<Object[]> rows) {
try {
List<RelDataTypeField> fields = plan.getRowType().getFieldList();
List<ExprValue> results = convertRows(rows, fields);
Schema schema = buildSchema(fields);
Schema schema = buildSchema(fields, results);
profileCtx
.getOrCreateMetric(MetricName.EXECUTE)
.set(System.nanoTime() - execStart);
Expand Down Expand Up @@ -193,9 +194,9 @@ public void onFailure(Exception e) {

private QueryResponse buildProfiledResponse(RelNode plan, ProfiledResult result) {
List<RelDataTypeField> fields = plan.getRowType().getFieldList();
Schema schema = buildSchema(fields);
List<ExprValue> results =
result.rows() != null ? convertRows(result.rows(), fields) : List.of();
Schema schema = buildSchema(fields, results);
QueryResponse response = new QueryResponse(schema, results, Cursor.None);
response.setProfile(result.profile());
if (!result.isSuccess()) {
Expand Down Expand Up @@ -265,10 +266,20 @@ private static List<Object> stripEpochDatePrefixInList(List<?> list) {
return out;
}

private Schema buildSchema(List<RelDataTypeField> fields) {
private Schema buildSchema(List<RelDataTypeField> fields, List<ExprValue> results) {
List<Schema.Column> columns = new ArrayList<>();
for (RelDataTypeField field : fields) {
ExprType exprType = convertType(field.getType());
// A column whose planned type is ANY (e.g. the SCALAR_MAX/SCALAR_MIN UDFs declare ANY since
// they accept mixed numeric/string operands) maps to UNDEFINED. Recover the concrete type
// from the first row's runtime value, mirroring OpenSearchExecutionEngine.buildResultSet on
// the Calcite path so both routes report the same schema type.
if (exprType == ExprCoreType.UNDEFINED && !results.isEmpty()) {
ExprValue cell = results.getFirst().tupleValue().get(field.getName());
if (cell != null && !cell.isNull() && !cell.isMissing()) {
exprType = cell.type();
}
}
columns.add(new Schema.Column(field.getName(), null, exprType));
}
return new Schema(columns);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,39 @@ void executeRelNode_numericTypes() {
6.0, response.getResults().get(0).tupleValue().get("d").value(), "double value. " + dump);
}

/**
* A column whose planned type is ANY (e.g. the SCALAR_MAX/SCALAR_MIN UDFs declare ANY) maps to
* UNDEFINED; recover the concrete type from the first row's runtime value so the schema matches
* the Calcite path instead of reporting "undefined".
*/
@Test
void executeRelNode_anyColumnRecoversTypeFromFirstRow() {
RelNode relNode = mockRelNode("age", SqlTypeName.BIGINT, "new", SqlTypeName.ANY);
Iterable<Object[]> rows = Arrays.asList(new Object[] {2L, 3}, new Object[] {4L, 4});
stubExecutorWith(relNode, rows);

QueryResponse response = executeAndCapture(relNode);
String dump = dumpResponse(response);

// 'new' is ANY in the plan but the first row's value is an Integer → schema reports INTEGER.
assertEquals(ExprCoreType.LONG, response.getSchema().getColumns().get(0).getExprType(), dump);
assertEquals(
ExprCoreType.INTEGER, response.getSchema().getColumns().get(1).getExprType(), dump);
}

/** ANY column with no rows to derive from stays UNDEFINED (mirrors the Calcite path fallback). */
@Test
void executeRelNode_anyColumnEmptyResultsStaysUndefined() {
RelNode relNode = mockRelNode("new", SqlTypeName.ANY);
stubExecutorWith(relNode, Collections.emptyList());

QueryResponse response = executeAndCapture(relNode);
String dump = dumpResponse(response);

assertEquals(
ExprCoreType.UNDEFINED, response.getSchema().getColumns().get(0).getExprType(), dump);
}

@Test
void executeRelNode_temporalTypes() {
RelNode relNode =
Expand Down
12 changes: 12 additions & 0 deletions integ-test/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,18 @@ task integTestRemote(type: RestIntegTestTask) {
// - earliest('now', utc_timestamp()): 'now' and utc_timestamp() resolve to the same
// instant on the route (true) but differ on v2 (false).
excludeTestsMatching '*CalcitePPLConditionBuiltinFunctionIT.testEarliestWithEval'

// === Excludes: CalcitePPLEvalMaxMinFunctionIT route divergences ===
// The AnalyticsExecutionEngine ANY-rescue (this PR) fixes the homogeneous max()/min()
// schema types, so most of the class now passes on the AE route. Three tests still
// diverge; each carries an in-test assumeNotAnalytics(...) (see AnalyticsRouteLimitation).
// - Mixed numeric+string operands throw 'Cannot infer return type for GREATEST' (the
// DataFusion GREATEST/LEAST can't unify heterogeneous operand types).
excludeTestsMatching '*CalcitePPLEvalMaxMinFunctionIT.testEvalMaxNumericAndString'
excludeTestsMatching '*CalcitePPLEvalMaxMinFunctionIT.testEvalMinNumericAndString'
// - max() over int operands reports bigint on the AE route (DataFusion widens integers
// to Int64) where the v2/Calcite path reports int.
excludeTestsMatching '*CalcitePPLEvalMaxMinFunctionIT.testEvalMaxNumeric'
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_DOG;
import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_NULL_MISSING;
import static org.opensearch.sql.util.AnalyticsRouteLimitation.EVAL_MAX_MIN_INT_WIDENING;
import static org.opensearch.sql.util.AnalyticsRouteLimitation.EVAL_MAX_MIN_MIXED_TYPES;
import static org.opensearch.sql.util.MatcherUtils.rows;
import static org.opensearch.sql.util.MatcherUtils.schema;
import static org.opensearch.sql.util.MatcherUtils.verifyDataRows;
Expand All @@ -28,6 +30,8 @@ public void init() throws Exception {

@Test
public void testEvalMaxNumeric() throws Exception {
// max(1, 3, age) selects an int-valued result; the AE route reports it as bigint.
assumeNotAnalytics(EVAL_MAX_MIN_INT_WIDENING);
JSONObject result =
executeQuery(
String.format(
Expand All @@ -49,6 +53,8 @@ public void testEvalMaxString() throws Exception {

@Test
public void testEvalMaxNumericAndString() throws Exception {
// Mixed numeric+string operands -> 'Cannot infer return type for GREATEST' on the AE route.
assumeNotAnalytics(EVAL_MAX_MIN_MIXED_TYPES);
JSONObject result =
executeQuery(
String.format(
Expand Down Expand Up @@ -83,6 +89,8 @@ public void testEvalMinString() throws Exception {

@Test
public void testEvalMinNumericAndString() throws Exception {
// Mixed numeric+string operands -> 'Cannot infer return type for GREATEST' on the AE route.
assumeNotAnalytics(EVAL_MAX_MIN_MIXED_TYPES);
JSONObject result =
executeQuery(
String.format(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,31 @@ public enum AnalyticsRouteLimitation {
EARLIEST_LATEST_NOW_CLOCK(
"earliest/latest with relative-time 'now' against utc_timestamp() diverges on the"
+ " analytics-engine route: 'now' and utc_timestamp() resolve to the same instant"
+ " (earliest('now', now) is true), but differ on the v2/Calcite path (false).");
+ " (earliest('now', now) is true), but differ on the v2/Calcite path (false)."),

/**
* eval {@code max(...)} / {@code min(...)} over operands that mix numeric and string types (e.g.
* {@code max(14, age, 'Fred', holdersName)}) throws {@code SqlValidatorException: Cannot infer
* return type for GREATEST} on the analytics-engine route. The SCALAR_MAX/SCALAR_MIN UDF is
* converted to a DataFusion {@code GREATEST}/{@code LEAST} during Substrait conversion, and that
* operator can't unify heterogeneous operand types. The v2/Calcite path applies OpenSearch's
* mixed-type comparison (strings sort above numbers) and returns a result.
*/
EVAL_MAX_MIN_MIXED_TYPES(
"eval max()/min() over mixed numeric+string operands throws 'Cannot infer return type for"
+ " GREATEST' on the analytics-engine route: the DataFusion GREATEST/LEAST can't unify"
+ " heterogeneous operand types."),

/**
* eval {@code max(...)} / {@code min(...)} over all-integer operands reports the result column as
* {@code bigint} on the analytics-engine route — DataFusion widens integer results to Int64 —
* whereas the v2/Calcite path reports {@code int} when the selected value fits in an int. The
* values match; only the schema's integer width differs.
*/
EVAL_MAX_MIN_INT_WIDENING(
"eval max()/min() over integer operands reports the result column as bigint on the"
+ " analytics-engine route (DataFusion widens integers to Int64), whereas the v2/Calcite"
+ " path reports int.");

private final String reason;

Expand Down
Loading