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 @@ -92,6 +92,36 @@ public void testIsNotNull() throws IOException {
rows(""));
}

@Test
public void testIsNotNullWithMultipleNotEquals() throws IOException {
// Verifies isnotnull() correctly filters null values when combined with multiple != conditions.
JSONObject actual =
executeQuery(
String.format(
"source=%s | where name != 'Jake' and name != 'Hello' and isnotnull(name)"
+ " | fields name",
TEST_INDEX_STATE_COUNTRY_WITH_NULL));

verifySchema(actual, schema("name", "string"));

verifyDataRows(actual, rows("John"), rows("Jane"), rows("Kevin"), rows(" "), rows(""));
}

@Test
public void testIsNotNullWithSingleNotEquals() throws IOException {
// Verifies isnotnull() correctly filters null values when combined with a single != condition.
JSONObject actual =
executeQuery(
String.format(
"source=%s | where name != 'Jake' and isnotnull(name) | fields name",
TEST_INDEX_STATE_COUNTRY_WITH_NULL));

verifySchema(actual, schema("name", "string"));

verifyDataRows(
actual, rows("John"), rows("Jane"), rows("Hello"), rows("Kevin"), rows(" "), rows(""));
}

@Test
public void testIsNotNullWithStruct() throws IOException {
JSONObject actual = executeQuery("source=big5 | where isnotnull(aws) | fields aws");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -712,9 +712,14 @@ private QueryExpression binary(RexCall call) {
QueryExpression finalExpression =
switch (nullAs) {
// e.g. where isNotNull(a) and ( a = 1 or a = 2)
// For this case, return `expression` is equivalent
// But DSL `bool.must` could slow down the query, so we return `expression`
case FALSE -> expression;
// For positive matches (IN), the expression naturally excludes nulls.
// For negations (NOT IN, ranges), we must add an exists check
// to ensure null values are filtered out.
case FALSE ->
isSearchWithPoints(call)
? expression
: CompoundQueryExpression.and(
false, expression, QueryExpression.create(pair.getKey()).exists());
// e.g. where isNull(a) or a = 1 or a = 2
case TRUE ->
CompoundQueryExpression.or(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import static org.mockito.Mockito.spy;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableRangeSet;
import com.google.common.collect.Range;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
Expand All @@ -23,10 +25,12 @@
import org.apache.calcite.rex.RexInputRef;
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.rex.RexUnknownAs;
import org.apache.calcite.runtime.Hook;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.util.Holder;
import org.apache.calcite.util.Sarg;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.opensearch.index.query.BoolQueryBuilder;
Expand Down Expand Up @@ -1185,4 +1189,60 @@ void isTrue_booleanFieldCombinedWithOtherCondition_generatesCompoundQuery()
""",
result.toString());
}

@Test
void search_complementedPointsWithNullAsFalse_generatesExistsAndNotInQuery()
throws ExpressionNotAnalyzableException {
// Simulates: a != 12 AND a != 13 AND isnotnull(a)
// Calcite merges this into SEARCH($0, Sarg[...; NULL AS FALSE]) with complemented points
Sarg<BigDecimal> sarg =
Sarg.of(
RexUnknownAs.FALSE,
ImmutableRangeSet.<BigDecimal>builder()
.add(Range.lessThan(BigDecimal.valueOf(12)))
.add(Range.open(BigDecimal.valueOf(12), BigDecimal.valueOf(13)))
.add(Range.greaterThan(BigDecimal.valueOf(13)))
.build());
RexNode sargLiteral =
builder.makeSearchArgumentLiteral(sarg, typeFactory.createSqlType(SqlTypeName.DECIMAL));
RexNode call = builder.makeCall(SqlStdOperatorTable.SEARCH, field1, sargLiteral);
QueryBuilder result = PredicateAnalyzer.analyze(call, schema, fieldTypes);

assertInstanceOf(BoolQueryBuilder.class, result);
assertEquals(
"""
{
"bool" : {
"must" : [
{
"bool" : {
"must_not" : [
{
"terms" : {
"a" : [
12.0,
13.0
],
"boost" : 1.0
}
}
],
"adjust_pure_negative" : true,
"boost" : 1.0
}
},
{
"exists" : {
"field" : "a",
"boost" : 1.0
}
}
],
"adjust_pure_negative" : true,
"boost" : 1.0
}
}\
""",
result.toString());
}
}
Loading