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 @@ -551,6 +551,35 @@ public void testMvindexRangeSingleElement() throws IOException {
verifyDataRows(actual, rows(List.of(3)));
}

@Test
public void testMvindexWithNonZeroIndexPushdown() throws IOException {
// Regression test for #5660: mvindex with non-zero literal index fails during pushdown
// because PLUS(1,1) gets widened to BIGINT but ITEM expects INTEGER.
JSONObject actual =
executeQuery(
String.format(
"source=%s | eval arr = array('a', 'b', 'c'), result = mvindex(arr, 2)"
+ " | head 1 | fields result",
TEST_INDEX_BANK));

verifySchema(actual, schema("result", "string"));
verifyDataRows(actual, rows("c"));
}

@Test
public void testMvindexWithStatsAggregationPushdown() throws IOException {
// Regression test for #5660: mvindex with non-zero index in aggregation context
// triggers pushdown script compilation where BIGINT/INTEGER mismatch occurred.
JSONObject actual =
executeQuery(
String.format(
"source=%s | eval arr = array('x', 'y', 'z'), e = mvindex(arr, 1)"
+ " | stats count() by e",
TEST_INDEX_BANK));

verifySchema(actual, schema("count()", "bigint"), schema("e", "string"));
}

@Test
public void testMvfindWithMatch() throws IOException {
JSONObject actual =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,19 @@ public RexNode visitCall(final RexCall call, ScriptParameterHelper helper) {
// Do normalization before standardization
Pair<SqlOperator, List<RexNode>> normalized = RexNormalize.normalize(call.op, call.operands);
List<RexNode> standardizedOperands = visitList(normalized.right, helper, update);
return helper.rexBuilder.makeCall(call.getType(), normalized.left, standardizedOperands);
RexNode result =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to Chen's comment - lets add more testing coverage on the change

helper.rexBuilder.makeCall(call.getType(), normalized.left, standardizedOperands);

if (allowNumericTypeWiden

@dai-chen dai-chen Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. The changes in this visitCall may have broader impact. Just wonder is it possible to narrow down and only apply the fix for mvindex (ITEM)? Or we confirm this bug happens for any function?
  2. Also please add test in CalciteArrayFunctionIT to verify.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To narrow down the fix for just mvindex or (ITEM) would not work, it has been tried in #5670 where the fix is done in the plan-layer where it the CAST added in the MVIndexFunctionImp does nothing during serialzation. This fix only wraps in CAST when the call is BINARY_ARITHMETIC, exact-numeric, and narrower than BIGINT. Comparisons (return BOOLEAN) and already-BIGINT arithmetic are unaffected. Added integration tests in CalciteArrayFunctionIT to verify.

&& SqlTypeUtil.isExactNumeric(call.getType())
&& !call.getType().getSqlTypeName().equals(SqlTypeName.BIGINT)) {
RelDataType targetType =
OpenSearchTypeFactory.TYPE_FACTORY.createTypeWithNullability(
call.getType(), call.getType().isNullable());
result = helper.rexBuilder.makeCast(targetType, result);
}

return result;
} finally {
helper.stack.pop();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,67 @@ void testSerializeAndDeserializeSearch() {
assertEquals(List.of(20, "Number", "Number", 10), helper.digests);
}

@Test
void testArithmeticInItemIndexPreservesIntegerType() {
// Simulates the mvindex(entity, 1) scenario where PLUS(1, 1) is used as an array index.
// The PLUS operands get widened to BIGINT during standardization (for doc-value compat),
// but the result must remain INTEGER since ITEM/arrayItemOptional expects int.
RelDataType intType = TYPE_FACTORY.createSqlType(SqlTypeName.INTEGER);
RexNode plusCall =
rexBuilder.makeCall(
SqlStdOperatorTable.PLUS,
rexBuilder.makeLiteral(1, TYPE_FACTORY.createSqlType(SqlTypeName.INTEGER)),
rexBuilder.makeLiteral(1, TYPE_FACTORY.createSqlType(SqlTypeName.INTEGER)));

final ScriptParameterHelper helper =
new ScriptParameterHelper(rowType.getFieldList(), fieldTypes, rexBuilder);
String code = serializer.serialize(plusCall, helper);
RexNode deserialized = serializer.deserialize(code);

// After round-trip, the result should be wrapped in CAST(INTEGER) to preserve the type
assertEquals(SqlTypeName.INTEGER, deserialized.getType().getSqlTypeName());
}

@Test
void testArithmeticBigintNotWrappedInCast() {
// When the original arithmetic type is already BIGINT, no cast should be added.
RexNode plusCall =
rexBuilder.makeCall(
SqlStdOperatorTable.PLUS,
rexBuilder.makeLiteral(1L, TYPE_FACTORY.createSqlType(SqlTypeName.BIGINT)),
rexBuilder.makeLiteral(1L, TYPE_FACTORY.createSqlType(SqlTypeName.BIGINT)));

final ScriptParameterHelper helper =
new ScriptParameterHelper(rowType.getFieldList(), fieldTypes, rexBuilder);
String code = serializer.serialize(plusCall, helper);
RexNode deserialized = serializer.deserialize(code);

// BIGINT arithmetic should stay BIGINT without extra CAST
assertEquals(SqlTypeName.BIGINT, deserialized.getType().getSqlTypeName());
}

@Test
void testArithmeticWithFieldPreservesIntegerType() {
// Simulates arithmetic with a field reference: Number + 1
// The field gets widened to BIGINT, but the original PLUS type is INTEGER,
// so the result should be cast back to INTEGER.
RexNode plusCall =
rexBuilder.makeCall(
SqlStdOperatorTable.PLUS,
rexBuilder.makeInputRef(rowType.getFieldList().get(1).getType(), 1),
rexBuilder.makeLiteral(1, TYPE_FACTORY.createSqlType(SqlTypeName.INTEGER)));

Map<String, ExprType> fieldTypesWithNumber =
Map.of("Referer", ExprCoreType.STRING, "Number", ExprCoreType.INTEGER);
final ScriptParameterHelper helper =
new ScriptParameterHelper(rowType.getFieldList(), fieldTypesWithNumber, rexBuilder);
String code = serializer.serialize(plusCall, helper);
RexNode deserialized = serializer.deserialize(code);

// Result should be INTEGER after deserialization
assertEquals(SqlTypeName.INTEGER, deserialized.getType().getSqlTypeName());
}

@Test
void deserialize_rejects_disallowed_class() throws Exception {
java.io.ByteArrayOutputStream output = new java.io.ByteArrayOutputStream();
Expand Down
Loading