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
@@ -0,0 +1,68 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.calcite.remote;

import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_LOGS;
import static org.opensearch.sql.util.MatcherUtils.rows;
import static org.opensearch.sql.util.MatcherUtils.schema;
import static org.opensearch.sql.util.MatcherUtils.verifyDataRows;
import static org.opensearch.sql.util.MatcherUtils.verifySchema;

import java.io.IOException;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import org.opensearch.sql.ppl.PPLIntegTestCase;

/**
* Tests partial filter pushdown when some predicates can be pushed natively and others require
* script evaluation.
*
* <p>Regression test for issue where LIKE on text fields (without .keyword subfield) caused entire
* AND filter to fall back to script, preventing timestamp range pushdown.
*/
public class CalcitePartialFilterPushdownIT extends PPLIntegTestCase {

@Override
public void init() throws Exception {
super.init();
enableCalcite();
loadIndex(Index.LOGS);
}

@Test
public void testTimestampRangePushesWithUnpushableLike() throws IOException {
// message is text field without .keyword — LIKE on it requires script evaluation
// @timestamp is date field — range should push natively despite LIKE failing
String query =
String.format(
"source=%s | where `@timestamp` >= '2023-01-01' and `@timestamp` < '2023-01-04' "
+ "and LIKE(message, '%%failed%%') | stats count()",
TEST_INDEX_LOGS);

JSONObject result = executeQuery(query);

// Just verify query executes and returns reasonable results
// The key regression is that this doesn't do a full table scan
verifySchema(result, schema("count()", "bigint"));
// Should find "Database connection failed" in the date range
verifyDataRows(result, rows(1L));
}

@Test
public void testMultipleUnpushablePredicatesInAnd() throws IOException {
// Both LIKE conditions are on text field, but timestamp should still push
String query =
String.format(
"source=%s | where `@timestamp` >= '2023-01-01' and LIKE(message, '%%space%%') "
+ "and LIKE(message, '%%low%%') | stats count()",
TEST_INDEX_LOGS);

JSONObject result = executeQuery(query);
verifySchema(result, schema("count()", "bigint"));
// Should find "Disk space low"
verifyDataRows(result, rows(1L));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1394,7 +1394,7 @@ public QueryExpression like(LiteralExpression literal, boolean caseSensitive) {
.caseInsensitive(!caseSensitive);
return this;
}
throw new UnsupportedOperationException("Like query is not supported for text field");
throw new PredicateAnalyzerException("Like query is not supported for text field");
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1649,4 +1649,48 @@ void notIsFalse_generatesOnlyMustNotTerm() throws ExpressionNotAnalyzableExcepti
""",
result.toString());
}

@Test
void andWithUnpushableLike_partiallyPushesOtherPredicates()
throws ExpressionNotAnalyzableException {
// field3 (c) is text without .keyword → LIKE throws PredicateAnalyzerException
// field4 (d) is date → timestamp range should push as RangeQueryBuilder
final RelDataType rowType =
builder
.getTypeFactory()
.builder()
.kind(StructKind.FULLY_QUALIFIED)
.add("a", typeFactory.createSqlType(SqlTypeName.INTEGER))
.add("b", typeFactory.createSqlType(SqlTypeName.VARCHAR))
.add("c", typeFactory.createSqlType(SqlTypeName.VARCHAR))
.add("d", typeFactory.createUDT(ExprUDT.EXPR_TIMESTAMP))
.add("e", typeFactory.createSqlType(SqlTypeName.BOOLEAN))
.build();
Hook.CURRENT_TIME.addThread((Consumer<Holder<Long>>) h -> h.set(0L));

RexInputRef field3 = builder.makeInputRef(typeFactory.createSqlType(SqlTypeName.VARCHAR), 2);
RexNode likeCall =
builder.makeCall(
SqlStdOperatorTable.LIKE, field3, stringLiteral, builder.makeLiteral("\\"));
RexNode rangeCall =
builder.makeCall(SqlStdOperatorTable.GREATER_THAN_OR_EQUAL, field4, dateTimeLiteral);
RexNode andCall = builder.makeCall(SqlStdOperatorTable.AND, rangeCall, likeCall);

QueryBuilder result = PredicateAnalyzer.analyze(andCall, schema, fieldTypes, rowType, cluster);

// Should be a BoolQueryBuilder with range in must[] and LIKE as script
assertInstanceOf(BoolQueryBuilder.class, result);
BoolQueryBuilder boolQuery = (BoolQueryBuilder) result;
assertEquals(2, boolQuery.must().size());

// First must clause should be the range query (pushable)
QueryBuilder firstMust = boolQuery.must().get(0);
assertInstanceOf(RangeQueryBuilder.class, firstMust);
RangeQueryBuilder rangeQuery = (RangeQueryBuilder) firstMust;
assertEquals("d", rangeQuery.fieldName());

// Second must clause should be script query (unpushable LIKE)
QueryBuilder secondMust = boolQuery.must().get(1);
assertInstanceOf(ScriptQueryBuilder.class, secondMust);
}
}
Loading