Skip to content

Commit af73737

Browse files
committed
Force script fallback for nested IS NULL / IS NOT NULL
ExistsQuery.canSupport() already returns false for nested references, but the default branch of FilterQueryBuilder.visitFunction() has a second dispatch: when canSupport() is false, it checks isNestedPredicate() and, if true, routes to NestedQuery.buildNested(func, query). That helper reads func.getArguments().get(1) on the assumption of a binary predicate shape, so unary IS NULL / IS NOT NULL would throw at runtime. Override isNestedPredicate() to return false in ExistsQuery so the dispatch falls through to the compiled-script path for nested-field predicates. Add unit tests covering both IS NULL and IS NOT NULL to lock in the fallback. Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
1 parent cbb928b commit af73737

2 files changed

Lines changed: 56 additions & 0 deletions

File tree

opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/filter/lucene/ExistsQuery.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,17 @@ public boolean canSupport(FunctionExpression func) {
4545
&& !isNestedFunction(func.getArguments().get(0));
4646
}
4747

48+
/**
49+
* Unary IS NULL / IS NOT NULL has no {@code arg[1]}, so we must never route through {@link
50+
* org.opensearch.sql.opensearch.storage.script.filter.lucene.NestedQuery#buildNested} — that path
51+
* reads {@code func.getArguments().get(1)} and would throw. Returning {@code false} here forces
52+
* {@code FilterQueryBuilder} to fall back to the script-query path for nested-field predicates.
53+
*/
54+
@Override
55+
public boolean isNestedPredicate(FunctionExpression func) {
56+
return false;
57+
}
58+
4859
@Override
4960
public QueryBuilder build(FunctionExpression func) {
5061
ReferenceExpression ref = (ReferenceExpression) func.getArguments().get(0);

opensearch/src/test/java/org/opensearch/sql/opensearch/storage/script/filter/FilterQueryBuilderTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,51 @@ void should_build_must_not_exists_query_for_is_null() {
205205
buildQuery(DSL.is_null(ref("age", INTEGER))));
206206
}
207207

208+
@Test
209+
void should_fallback_to_script_for_nested_is_not_null() {
210+
// Nested IS_NOT_NULL must NOT route through NestedQuery.buildNested(): that path reads
211+
// arg[1] and unary IS_NOT_NULL only has arg[0]. ExistsQuery.isNestedPredicate() returns
212+
// false precisely to force the script fallback here.
213+
mockToStringSerializer();
214+
assertJsonEquals(
215+
"{\n"
216+
+ " \"script\" : {\n"
217+
+ " \"script\" : {\n"
218+
+ " \"source\" :"
219+
+ " \"{\\\"langType\\\":\\\"v2\\\",\\\"script\\\":\\\"is"
220+
+ " not null(FunctionExpression(functionName=nested, arguments=[message.info,"
221+
+ " message]))\\\"}\",\n"
222+
+ " \"lang\" : \"opensearch_compounded_script\"\n"
223+
+ " },\n"
224+
+ " \"boost\" : 1.0\n"
225+
+ " }\n"
226+
+ "}",
227+
buildQuery(
228+
DSL.isnotnull(
229+
DSL.nested(DSL.ref("message.info", STRING), DSL.ref("message", STRING)))));
230+
}
231+
232+
@Test
233+
void should_fallback_to_script_for_nested_is_null() {
234+
// Symmetric to the IS_NOT_NULL case: must not crash with an arg[1] lookup via NestedQuery.
235+
mockToStringSerializer();
236+
assertJsonEquals(
237+
"{\n"
238+
+ " \"script\" : {\n"
239+
+ " \"script\" : {\n"
240+
+ " \"source\" :"
241+
+ " \"{\\\"langType\\\":\\\"v2\\\",\\\"script\\\":\\\"is"
242+
+ " null(FunctionExpression(functionName=nested, arguments=[message.info,"
243+
+ " message]))\\\"}\",\n"
244+
+ " \"lang\" : \"opensearch_compounded_script\"\n"
245+
+ " },\n"
246+
+ " \"boost\" : 1.0\n"
247+
+ " }\n"
248+
+ "}",
249+
buildQuery(
250+
DSL.is_null(DSL.nested(DSL.ref("message.info", STRING), DSL.ref("message", STRING)))));
251+
}
252+
208253
@Test
209254
void should_build_script_query_for_function_expression() {
210255
mockToStringSerializer();

0 commit comments

Comments
 (0)