Skip to content
Closed
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 @@ -317,7 +317,7 @@ private static Optional<RexNode> replaceWithNullLiteralInCoalesce(CalcitePlanCon
if (context.isInCoalesceFunction()) {
return Optional.of(
context.rexBuilder.makeNullLiteral(
context.rexBuilder.getTypeFactory().createSqlType(SqlTypeName.VARCHAR)));
context.rexBuilder.getTypeFactory().createSqlType(SqlTypeName.NULL)));
}
return Optional.empty();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.calcite.adapter.enumerable.NotNullImplementor;
import org.apache.calcite.adapter.enumerable.NullPolicy;
import org.apache.calcite.linq4j.tree.Expression;
Expand Down Expand Up @@ -93,11 +94,29 @@ public SqlReturnTypeInference getReturnTypeInference() {
return opBinding -> {
var operandTypes = opBinding.collectOperandTypes();

// Let Calcite determine the least restrictive common type
var commonType = opBinding.getTypeFactory().leastRestrictive(operandTypes);
return commonType != null
? commonType
: opBinding.getTypeFactory().createSqlType(SqlTypeName.VARCHAR);
// Filter out NULL-typed operands so they don't influence type inference.
// NULL is Calcite's bottom type and should be compatible with any type;
// without filtering, leastRestrictive may fall back to VARCHAR.
var nonNullTypes =
operandTypes.stream()
.filter(t -> t.getSqlTypeName() != SqlTypeName.NULL)
.collect(Collectors.toList());

if (nonNullTypes.isEmpty()) {
// All operands are NULL — return nullable NULL type
return opBinding
.getTypeFactory()
.createTypeWithNullability(
opBinding.getTypeFactory().createSqlType(SqlTypeName.NULL), true);
}

// Let Calcite determine the least restrictive common type among non-null operands
var commonType = opBinding.getTypeFactory().leastRestrictive(nonNullTypes);
if (commonType != null) {
// Ensure the result is nullable since COALESCE can return NULL
return opBinding.getTypeFactory().createTypeWithNullability(commonType, true);
}
return opBinding.getTypeFactory().createSqlType(SqlTypeName.VARCHAR);
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,48 @@ public void testLeastRestrictiveDelegatesToSuperForPlainTypes() {
assertNotNull(result);
assertEquals(SqlTypeName.INTEGER, result.getSqlTypeName());
}

@Test
public void testLeastRestrictiveNullAndIntegerReturnsInteger() {
RelDataType nullType = TYPE_FACTORY.createSqlType(SqlTypeName.NULL);
RelDataType intType = TYPE_FACTORY.createSqlType(SqlTypeName.INTEGER);

RelDataType result = TYPE_FACTORY.leastRestrictive(List.of(nullType, intType));

assertNotNull("leastRestrictive(NULL, INTEGER) should not be null", result);
assertEquals(SqlTypeName.INTEGER, result.getSqlTypeName());
}

@Test
public void testLeastRestrictiveNullAndDecimalReturnsDecimal() {
RelDataType nullType = TYPE_FACTORY.createSqlType(SqlTypeName.NULL);
RelDataType decType = TYPE_FACTORY.createSqlType(SqlTypeName.DECIMAL, 10, 0);

RelDataType result = TYPE_FACTORY.leastRestrictive(List.of(nullType, decType));

assertNotNull("leastRestrictive(NULL, DECIMAL) should not be null", result);
assertEquals(SqlTypeName.DECIMAL, result.getSqlTypeName());
}

@Test
public void testLeastRestrictiveNullAndBooleanReturnsBoolean() {
RelDataType nullType = TYPE_FACTORY.createSqlType(SqlTypeName.NULL);
RelDataType boolType = TYPE_FACTORY.createSqlType(SqlTypeName.BOOLEAN);

RelDataType result = TYPE_FACTORY.leastRestrictive(List.of(nullType, boolType));

assertNotNull("leastRestrictive(NULL, BOOLEAN) should not be null", result);
assertEquals(SqlTypeName.BOOLEAN, result.getSqlTypeName());
}

@Test
public void testLeastRestrictiveMultipleNullsAndIntegerReturnsInteger() {
RelDataType nullType = TYPE_FACTORY.createSqlType(SqlTypeName.NULL);
RelDataType intType = TYPE_FACTORY.createSqlType(SqlTypeName.INTEGER);

RelDataType result = TYPE_FACTORY.leastRestrictive(List.of(nullType, nullType, intType));

assertNotNull("leastRestrictive(NULL, NULL, INTEGER) should not be null", result);
assertEquals(SqlTypeName.INTEGER, result.getSqlTypeName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,43 @@ public void testCoalesceWithCompatibleNumericAndTemporalTypes() throws IOExcepti
schema("result", "int"));
verifyDataRows(actual, rows(70, 2023, 4, 70), rows(30, 2023, 4, 30));
}

/** Issue #5175: COALESCE(null, 42) should return int type, not string. */
@Test
public void testCoalesceNullLiteralWithIntegerReturnsIntegerType() throws IOException {
JSONObject actual =
executeQuery(
String.format(
"source=%s | eval x = coalesce(null, 42) | fields x | head 1",
TEST_INDEX_STATE_COUNTRY_WITH_NULL));

verifySchema(actual, schema("x", "int"));
verifyDataRows(actual, rows(42));
}

/** Issue #5175: COALESCE(42, null) should return int type, not string. */
@Test
public void testCoalesceIntegerWithNullLiteralReturnsIntegerType() throws IOException {
JSONObject actual =
executeQuery(
String.format(
"source=%s | eval x = coalesce(42, null) | fields x | head 1",
TEST_INDEX_STATE_COUNTRY_WITH_NULL));

verifySchema(actual, schema("x", "int"));
verifyDataRows(actual, rows(42));
}

/** Issue #5175: COALESCE(nonexistent_field, 42) should return int type, not string. */
@Test
public void testCoalesceNonExistentFieldWithIntegerReturnsIntegerType() throws IOException {
JSONObject actual =
executeQuery(
String.format(
"source=%s | eval x = coalesce(nonexistent_field, 42) | fields x | head 1",
TEST_INDEX_STATE_COUNTRY_WITH_NULL));

verifySchema(actual, schema("x", "int"));
verifyDataRows(actual, rows(42));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
setup:
- do:
query.settings:
body:
transient:
plugins.calcite.enabled: true

- do:
indices.create:
index: issue5175
body:
settings:
number_of_shards: 1
number_of_replicas: 0
mappings:
properties:
dummy:
type: keyword

- do:
bulk:
refresh: true
body:
- '{"index": {"_index": "issue5175", "_id": "1"}}'
- '{"dummy": "row"}'

---
teardown:
- do:
indices.delete:
index: issue5175
ignore_unavailable: true
- do:
query.settings:
body:
transient:
plugins.calcite.enabled: false

---
"Issue 5175: COALESCE(null, 42) should return int type not string":
- skip:
features:
- headers
- do:
headers:
Content-Type: 'application/json'
ppl:
body:
query: source=issue5175 | eval x = coalesce(null, 42) | fields x | head 1

- match: { total: 1 }
- match: { schema: [ { name: x, type: int } ] }
- match: { datarows: [ [ 42 ] ] }

---
"Issue 5175: COALESCE(42, null) should return int type not string":
- skip:
features:
- headers
- do:
headers:
Content-Type: 'application/json'
ppl:
body:
query: source=issue5175 | eval x = coalesce(42, null) | fields x | head 1

- match: { total: 1 }
- match: { schema: [ { name: x, type: int } ] }
- match: { datarows: [ [ 42 ] ] }
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@

package org.opensearch.sql.ppl.calcite;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;

import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql.type.SqlTypeUtil;
import org.apache.calcite.test.CalciteAssert;
import org.junit.Test;

Expand Down Expand Up @@ -138,7 +145,7 @@ public void testCoalesceWithNonExistentField() {
RelNode root = getRelNode(ppl);
String expectedLogical =
"LogicalSort(fetch=[2])\n"
+ " LogicalProject(EMPNO=[$0], result=[COALESCE(null:VARCHAR, $1)])\n"
+ " LogicalProject(EMPNO=[$0], result=[COALESCE(null:NULL, $1)])\n"
+ " LogicalTableScan(table=[[scott, EMP]])\n";
verifyLogical(root, expectedLogical);

Expand All @@ -155,7 +162,7 @@ public void testCoalesceWithMultipleNonExistentFields() {
RelNode root = getRelNode(ppl);
String expectedLogical =
"LogicalSort(fetch=[1])\n"
+ " LogicalProject(EMPNO=[$0], result=[COALESCE(null:VARCHAR, null:VARCHAR, $1,"
+ " LogicalProject(EMPNO=[$0], result=[COALESCE(null:NULL, null:NULL, $1,"
+ " 'fallback':VARCHAR)])\n"
+ " LogicalTableScan(table=[[scott, EMP]])\n";
verifyLogical(root, expectedLogical);
Expand All @@ -175,8 +182,8 @@ public void testCoalesceWithAllNonExistentFields() {
RelNode root = getRelNode(ppl);
String expectedLogical =
"LogicalSort(fetch=[1])\n"
+ " LogicalProject(EMPNO=[$0], result=[COALESCE(null:VARCHAR, null:VARCHAR,"
+ " null:VARCHAR)])\n"
+ " LogicalProject(EMPNO=[$0], result=[COALESCE(null:NULL, null:NULL,"
+ " null:NULL)])\n"
+ " LogicalTableScan(table=[[scott, EMP]])\n";
verifyLogical(root, expectedLogical);

Expand Down Expand Up @@ -235,4 +242,89 @@ public void testCoalesceTypeInferenceWithNonNullableOperands() {
+ "LIMIT 2";
verifyPPLToSparkSQL(root, expectedSparkSql);
}

/** Verifies that COALESCE(null, 42) infers a numeric type, not VARCHAR (issue #5175). */
@Test
public void testCoalesceNullLiteralWithIntegerPreservesIntegerType() {
String ppl = "source=EMP | eval x = coalesce(null, 42) | fields x | head 1";
RelNode root = getRelNode(ppl);
RelDataType rowType = root.getRowType();
RelDataType xType = rowType.getField("x", true, false).getType();
assertNotEquals(
"COALESCE(null, 42) must not return VARCHAR",
SqlTypeName.VARCHAR,
xType.getSqlTypeName());
assertTrue(
"COALESCE(null, 42) should return a numeric type, got " + xType.getSqlTypeName(),
SqlTypeUtil.isNumeric(xType));
}

/** Verifies that COALESCE(42, null) infers a numeric type, not VARCHAR. */
@Test
public void testCoalesceIntegerWithNullLiteralPreservesIntegerType() {
String ppl = "source=EMP | eval x = coalesce(42, null) | fields x | head 1";
RelNode root = getRelNode(ppl);
RelDataType rowType = root.getRowType();
RelDataType xType = rowType.getField("x", true, false).getType();
assertNotEquals(
"COALESCE(42, null) must not return VARCHAR",
SqlTypeName.VARCHAR,
xType.getSqlTypeName());
assertTrue(
"COALESCE(42, null) should return a numeric type, got " + xType.getSqlTypeName(),
SqlTypeUtil.isNumeric(xType));
}

/** Verifies that COALESCE(null, 42) returns numeric 42, not string "42". */
@Test
public void testCoalesceNullAndIntegerLiteralReturnsCorrectValue() {
String ppl = "source=EMP | eval x = coalesce(null, 42) | fields x | head 1";
RelNode root = getRelNode(ppl);
verifyResult(root, "x=42\n");
}

/** Verifies that COALESCE(null, 3.14) infers a numeric type. */
@Test
public void testCoalesceNullLiteralWithDoublePreservesNumericType() {
String ppl = "source=EMP | eval x = coalesce(null, 3.14) | fields x | head 1";
RelNode root = getRelNode(ppl);
RelDataType rowType = root.getRowType();
RelDataType xType = rowType.getField("x", true, false).getType();
assertNotEquals(
"COALESCE(null, 3.14) must not return VARCHAR",
SqlTypeName.VARCHAR,
xType.getSqlTypeName());
assertTrue(
"COALESCE(null, 3.14) should return a numeric type, got " + xType.getSqlTypeName(),
SqlTypeUtil.isNumeric(xType));
}

/** Verifies that COALESCE(null, null, 42) still returns a numeric type. */
@Test
public void testCoalesceMultipleNullsWithIntegerPreservesIntegerType() {
String ppl = "source=EMP | eval x = coalesce(null, null, 42) | fields x | head 1";
RelNode root = getRelNode(ppl);
RelDataType rowType = root.getRowType();
RelDataType xType = rowType.getField("x", true, false).getType();
assertNotEquals(
"COALESCE(null, null, 42) must not return VARCHAR",
SqlTypeName.VARCHAR,
xType.getSqlTypeName());
assertTrue(
"COALESCE(null, null, 42) should return a numeric type, got " + xType.getSqlTypeName(),
SqlTypeUtil.isNumeric(xType));
}

/** Verifies that COALESCE(null, true) returns BOOLEAN type. */
@Test
public void testCoalesceNullLiteralWithBooleanPreservesBooleanType() {
String ppl = "source=EMP | eval x = coalesce(null, true) | fields x | head 1";
RelNode root = getRelNode(ppl);
RelDataType rowType = root.getRowType();
RelDataType xType = rowType.getField("x", true, false).getType();
assertEquals(
"COALESCE(null, true) should return BOOLEAN type",
SqlTypeName.BOOLEAN,
xType.getSqlTypeName());
}
}
Loading