Skip to content

Commit f70bc97

Browse files
committed
Improve unit test coverage on validation logics
Signed-off-by: Yuanchun Shen <yuanchu@amazon.com>
1 parent 3443fae commit f70bc97

7 files changed

Lines changed: 194 additions & 5 deletions

File tree

core/src/main/java/org/opensearch/sql/calcite/utils/OpenSearchTypeFactory.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.util.Locale;
3636
import java.util.Map;
3737
import java.util.Map.Entry;
38+
import java.util.Objects;
3839
import lombok.Getter;
3940
import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
4041
import org.apache.calcite.rel.type.RelDataType;
@@ -332,7 +333,8 @@ public Type getJavaClass(RelDataType type) {
332333
@Override
333334
public @Nullable RelDataType leastRestrictive(List<RelDataType> types) {
334335
// Handle UDTs separately, otherwise the least restrictive type will become VARCHAR
335-
if (types.stream().anyMatch(OpenSearchTypeUtil::isUserDefinedType)) {
336+
if (types.stream().anyMatch(OpenSearchTypeUtil::isUserDefinedType)
337+
&& types.stream().allMatch(Objects::nonNull)) {
336338
int nullCount = 0;
337339
int anyCount = 0;
338340
int nullableCount = 0;

core/src/test/java/org/opensearch/sql/calcite/utils/OpenSearchTypeFactoryTest.java

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,13 +174,12 @@ public void testLeastRestrictive_ipAndBinary_returnsVarchar() {
174174
public void testLeastRestrictive_ipUdtAndOther_returnsIpUdt() {
175175
// When IP UDT is mixed with OTHER type (which is used as intermediate for IP)
176176
RelDataType ipUdt = TYPE_FACTORY.createUDT(ExprUDT.EXPR_IP);
177-
RelDataType nullType = TYPE_FACTORY.createSqlType(SqlTypeName.NULL);
177+
RelDataType otherType = TYPE_FACTORY.createSqlType(SqlTypeName.OTHER);
178178

179-
RelDataType result = TYPE_FACTORY.leastRestrictive(List.of(ipUdt, nullType));
179+
RelDataType result = TYPE_FACTORY.leastRestrictive(List.of(ipUdt, otherType));
180180

181181
assertNotNull(result);
182182
assertTrue(OpenSearchTypeUtil.isIp(result));
183-
assertTrue(result.isNullable());
184183
}
185184

186185
@Test
@@ -276,4 +275,33 @@ public void testLeastRestrictive_multipleNulls_returnsNullableUdt() {
276275
assertTrue(OpenSearchTypeUtil.isDate(result));
277276
assertTrue(result.isNullable());
278277
}
278+
279+
// ==================== leastRestrictive null/empty input tests ====================
280+
281+
@Test
282+
public void testLeastRestrictive_emptyList_throwsIllegalArgumentException() {
283+
// Empty list causes IllegalArgumentException from Calcite's base implementation
284+
org.junit.jupiter.api.Assertions.assertThrows(
285+
IllegalArgumentException.class, () -> TYPE_FACTORY.leastRestrictive(List.of()));
286+
}
287+
288+
@Test
289+
public void testLeastRestrictive_nullList_throwsNPE() {
290+
// Null list causes NullPointerException
291+
org.junit.jupiter.api.Assertions.assertThrows(
292+
NullPointerException.class, () -> TYPE_FACTORY.leastRestrictive(null));
293+
}
294+
295+
@Test
296+
public void testLeastRestrictive_listWithNullElement_throwsNPE() {
297+
RelDataType dateUdt = TYPE_FACTORY.createUDT(ExprUDT.EXPR_DATE);
298+
// List.of() doesn't allow null elements, so we use a different approach
299+
java.util.List<RelDataType> types = new java.util.ArrayList<>();
300+
types.add(dateUdt);
301+
types.add(null);
302+
303+
// List containing null element causes NPE
304+
org.junit.jupiter.api.Assertions.assertThrows(
305+
NullPointerException.class, () -> TYPE_FACTORY.leastRestrictive(types));
306+
}
279307
}

core/src/test/java/org/opensearch/sql/calcite/validate/OpenSearchSparkSqlDialectTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import static org.junit.jupiter.api.Assertions.assertEquals;
99
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
1010
import static org.junit.jupiter.api.Assertions.assertNotNull;
11+
import static org.junit.jupiter.api.Assertions.assertThrows;
1112
import static org.junit.jupiter.api.Assertions.assertTrue;
1213
import static org.opensearch.sql.calcite.utils.OpenSearchTypeFactory.TYPE_FACTORY;
1314

@@ -22,6 +23,7 @@
2223
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
2324
import org.apache.calcite.sql.parser.SqlParserPos;
2425
import org.apache.calcite.sql.pretty.SqlPrettyWriter;
26+
import org.apache.calcite.sql.type.SqlTypeName;
2527
import org.apache.calcite.sql.validate.SqlConformance;
2628
import org.junit.jupiter.api.Test;
2729
import org.opensearch.sql.calcite.utils.OpenSearchTypeFactory.ExprUDT;
@@ -108,4 +110,37 @@ public void testGetConformanceIsLiberal() {
108110
assertNotNull(conformance);
109111
assertTrue(conformance.isLiberal());
110112
}
113+
114+
@Test
115+
public void testGetCastSpecForNonIpType() {
116+
// Non-IP types should delegate to parent SparkSqlDialect
117+
RelDataType intType = TYPE_FACTORY.createSqlType(SqlTypeName.INTEGER);
118+
119+
SqlNode castSpec = OpenSearchSparkSqlDialect.DEFAULT.getCastSpec(intType);
120+
121+
// SparkSqlDialect returns a SqlDataTypeSpec for INTEGER type
122+
assertNotNull(castSpec);
123+
assertInstanceOf(SqlDataTypeSpec.class, castSpec);
124+
// It should NOT be the IP-specific spec
125+
SqlDataTypeSpec typeSpec = (SqlDataTypeSpec) castSpec;
126+
assertEquals("INTEGER", typeSpec.toString());
127+
}
128+
129+
@Test
130+
public void testGetCastSpecForVarcharType() {
131+
RelDataType varcharType = TYPE_FACTORY.createSqlType(SqlTypeName.VARCHAR);
132+
133+
SqlNode castSpec = OpenSearchSparkSqlDialect.DEFAULT.getCastSpec(varcharType);
134+
135+
// SparkSqlDialect handles VARCHAR specially, returns a SqlDataTypeSpec
136+
assertNotNull(castSpec);
137+
assertInstanceOf(SqlDataTypeSpec.class, castSpec);
138+
}
139+
140+
@Test
141+
public void testGetCastSpecForNullType() {
142+
// Null input should throw NullPointerException
143+
assertThrows(
144+
NullPointerException.class, () -> OpenSearchSparkSqlDialect.DEFAULT.getCastSpec(null));
145+
}
111146
}

core/src/test/java/org/opensearch/sql/calcite/validate/PplTypeCoercionRuleTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public void testVarcharToOtherCoercion() {
4242
SqlTypeCoercionRule rule = PplTypeCoercionRule.instance();
4343
Map<SqlTypeName, ImmutableSet<SqlTypeName>> mapping = rule.getTypeMapping();
4444

45-
// VARCHAR should be coercible from OTHER
45+
// VARCHAR and CHAR should be coercible to OTHER (for IP type support)
4646
ImmutableSet<SqlTypeName> otherCoercions = mapping.get(SqlTypeName.OTHER);
4747
assertNotNull(otherCoercions);
4848
assertTrue(otherCoercions.contains(SqlTypeName.VARCHAR));

core/src/test/java/org/opensearch/sql/calcite/validate/PplTypeCoercionTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ public void testImplicitCast_preservesNullability() {
110110
assertNotNull(nullableResult);
111111
assertNotNull(nonNullableResult);
112112
assertTrue(nullableResult.isNullable());
113+
assertFalse(nonNullableResult.isNullable());
113114
}
114115

115116
// ==================== commonTypeForBinaryComparison tests ====================

core/src/test/java/org/opensearch/sql/calcite/validate/ValidationUtilsTest.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,4 +230,35 @@ public void testTolerantValidationExceptionNullMessage() {
230230
Exception e = new RuntimeException();
231231
assertFalse(ValidationUtils.tolerantValidationException(e));
232232
}
233+
234+
@Test
235+
public void testTolerantValidationExceptionNullException() {
236+
assertThrows(
237+
NullPointerException.class, () -> ValidationUtils.tolerantValidationException(null));
238+
}
239+
240+
@Test
241+
public void testCreateUDTWithAttributesNullSourceType() {
242+
// When sourceType is null, syncAttributes handles it gracefully
243+
RelDataType dateUdt =
244+
ValidationUtils.createUDTWithAttributes(TYPE_FACTORY, null, ExprUDT.EXPR_DATE);
245+
assertNotNull(dateUdt);
246+
}
247+
248+
@Test
249+
public void testCreateUDTWithAttributesNullExprUDT() {
250+
RelDataType sourceType = TYPE_FACTORY.createSqlType(SqlTypeName.VARCHAR, true);
251+
assertThrows(
252+
NullPointerException.class,
253+
() -> ValidationUtils.createUDTWithAttributes(TYPE_FACTORY, sourceType, (ExprUDT) null));
254+
}
255+
256+
@Test
257+
public void testCreateUDTWithAttributesNullSqlTypeName() {
258+
RelDataType sourceType = TYPE_FACTORY.createSqlType(SqlTypeName.VARCHAR, true);
259+
assertThrows(
260+
NullPointerException.class,
261+
() ->
262+
ValidationUtils.createUDTWithAttributes(TYPE_FACTORY, sourceType, (SqlTypeName) null));
263+
}
233264
}

core/src/test/java/org/opensearch/sql/calcite/validate/shuttles/SkipRelValidationShuttleTest.java

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,96 @@ public void testWidthBucketWithDateTypeTriggersSkip() {
170170
SkipRelValidationShuttle.SKIP_CALLS.stream().anyMatch(p -> p.test(widthBucketCall));
171171
assertTrue(shouldSkip);
172172
}
173+
174+
@Test
175+
public void testWidthBucketWithNullOperandDoesNotTriggerSkip() {
176+
// Create WIDTH_BUCKET call with null literal as first operand
177+
RelDataType intType = TYPE_FACTORY.createSqlType(SqlTypeName.INTEGER);
178+
RexLiteral nullLiteral = rexBuilder.makeNullLiteral(intType);
179+
RexLiteral minVal = rexBuilder.makeExactLiteral(BigDecimal.valueOf(0), intType);
180+
RexLiteral maxVal = rexBuilder.makeExactLiteral(BigDecimal.valueOf(100), intType);
181+
RexLiteral buckets = rexBuilder.makeExactLiteral(BigDecimal.valueOf(10), intType);
182+
183+
RexCall widthBucketCall =
184+
(RexCall)
185+
rexBuilder.makeCall(
186+
PPLBuiltinOperators.WIDTH_BUCKET, nullLiteral, minVal, maxVal, buckets);
187+
188+
// Null operand should not trigger skip (it's not a datetime type)
189+
boolean shouldSkip =
190+
SkipRelValidationShuttle.SKIP_CALLS.stream().anyMatch(p -> p.test(widthBucketCall));
191+
assertFalse(shouldSkip);
192+
}
193+
194+
@Test
195+
public void testWidthBucketWithEmptyOperandsDoesNotTriggerSkip() {
196+
// Create mocked WIDTH_BUCKET call with empty operands
197+
RexCall call = mock(RexCall.class);
198+
when(call.getOperator()).thenReturn(PPLBuiltinOperators.WIDTH_BUCKET);
199+
when(call.getOperands()).thenReturn(List.of());
200+
201+
// Empty operands should not trigger skip
202+
boolean shouldSkip = SkipRelValidationShuttle.SKIP_CALLS.stream().anyMatch(p -> p.test(call));
203+
assertFalse(shouldSkip);
204+
}
205+
206+
@Test
207+
public void testSingleCaseInGroupByDoesNotTriggerSkip() {
208+
// Create mocked LogicalAggregate with single CASE expression in GROUP BY
209+
RelDataType intType = TYPE_FACTORY.createSqlType(SqlTypeName.INTEGER);
210+
211+
// Create single CASE expression
212+
RexInputRef ageRef = new RexInputRef(0, intType);
213+
RexLiteral literal30 = rexBuilder.makeExactLiteral(BigDecimal.valueOf(30), intType);
214+
215+
RexNode caseExpr =
216+
rexBuilder.makeCall(
217+
SqlStdOperatorTable.CASE,
218+
rexBuilder.makeCall(SqlStdOperatorTable.LESS_THAN, ageRef, literal30),
219+
rexBuilder.makeLiteral("young"),
220+
rexBuilder.makeLiteral("old"));
221+
222+
// Create non-case expression (just a literal)
223+
RexNode nonCaseExpr = rexBuilder.makeExactLiteral(BigDecimal.ONE, intType);
224+
225+
// Mock LogicalProject
226+
LogicalProject project = mock(LogicalProject.class);
227+
when(project.getProjects()).thenReturn(List.of(caseExpr, nonCaseExpr));
228+
229+
// Mock LogicalAggregate
230+
LogicalAggregate aggregate = mock(LogicalAggregate.class);
231+
when(aggregate.getGroupCount()).thenReturn(2);
232+
when(aggregate.getInput()).thenReturn(project);
233+
234+
// Test the predicate - should NOT trigger skip because only 1 CASE
235+
boolean shouldSkip =
236+
SkipRelValidationShuttle.SKIP_AGGREGATES.stream().anyMatch(p -> p.test(aggregate));
237+
assertFalse(shouldSkip);
238+
}
239+
240+
@Test
241+
public void testAggregateWithNonProjectInputDoesNotTriggerSkip() {
242+
// Mock LogicalAggregate with non-LogicalProject input
243+
LogicalAggregate aggregate = mock(LogicalAggregate.class);
244+
LogicalValues values = mock(LogicalValues.class);
245+
when(aggregate.getGroupCount()).thenReturn(2);
246+
when(aggregate.getInput()).thenReturn(values);
247+
248+
// Test the predicate - should NOT trigger skip because input is not LogicalProject
249+
boolean shouldSkip =
250+
SkipRelValidationShuttle.SKIP_AGGREGATES.stream().anyMatch(p -> p.test(aggregate));
251+
assertFalse(shouldSkip);
252+
}
253+
254+
@Test
255+
public void testAggregateWithSingleGroupDoesNotTriggerSkip() {
256+
// Mock LogicalAggregate with single group (groupCount = 1)
257+
LogicalAggregate aggregate = mock(LogicalAggregate.class);
258+
when(aggregate.getGroupCount()).thenReturn(1);
259+
260+
// Test the predicate - should NOT trigger skip because only 1 group
261+
boolean shouldSkip =
262+
SkipRelValidationShuttle.SKIP_AGGREGATES.stream().anyMatch(p -> p.test(aggregate));
263+
assertFalse(shouldSkip);
264+
}
173265
}

0 commit comments

Comments
 (0)