From 78817bca73cd405fad554e00435a6fdb59b2c84a Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Sat, 30 Aug 2025 09:29:25 +1200 Subject: [PATCH] #3666 Fix for NPE with filterMany() containing or() For FilterMany with QueryBeans, it creates a queryBean to build the filterMany predicates with. This query bean needed 2 changes for this fix: 1. Needs to set it's internal "root" such that it supports chaining (required for or() etc) 2. The ExpressionFactory needs to be explicitly passed to the expression list (rather than get it from the query which is actually null in this case). --- .../expression/DefaultExpressionFactory.java | 12 ++++++------ .../server/expression/DefaultExpressionList.java | 8 ++------ .../server/expression/JunctionExpression.java | 4 ++-- .../server/querydefn/DefaultOrmQuery.java | 6 +++--- .../main/java/io/ebean/typequery/QueryBean.java | 2 +- .../src/test/java/org/querytest/QCustomerTest.java | 14 ++++++++++++++ 6 files changed, 28 insertions(+), 18 deletions(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionFactory.java b/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionFactory.java index 2cb77ece6e..6d76fa9011 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionFactory.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionFactory.java @@ -726,7 +726,7 @@ public Expression not(Expression exp) { */ @Override public Junction conjunction(Query query) { - return new JunctionExpression<>(Junction.Type.AND, query, query.where()); + return new JunctionExpression<>(Junction.Type.AND, query, this, query.where()); } /** @@ -734,7 +734,7 @@ public Junction conjunction(Query query) { */ @Override public Junction disjunction(Query query) { - return new JunctionExpression<>(Junction.Type.OR, query, query.where()); + return new JunctionExpression<>(Junction.Type.OR, query, this, query.where()); } /** @@ -742,7 +742,7 @@ public Junction disjunction(Query query) { */ @Override public Junction conjunction(Query query, ExpressionList parent) { - return new JunctionExpression<>(Junction.Type.AND, query, parent); + return new JunctionExpression<>(Junction.Type.AND, query, this, parent); } /** @@ -750,14 +750,14 @@ public Junction conjunction(Query query, ExpressionList parent) { */ @Override public Junction disjunction(Query query, ExpressionList parent) { - return new JunctionExpression<>(Junction.Type.OR, query, parent); + return new JunctionExpression<>(Junction.Type.OR, query, this, parent); } /** * Return a list of expressions that are wrapped by NOT. */ public Junction junction(Junction.Type type, Query query) { - return new JunctionExpression<>(type, query, query.where()); + return new JunctionExpression<>(type, query, this, query.where()); } /** @@ -765,6 +765,6 @@ public Junction junction(Junction.Type type, Query query) { */ @Override public Junction junction(Junction.Type type, Query query, ExpressionList parent) { - return new JunctionExpression<>(type, query, parent); + return new JunctionExpression<>(type, query, this, parent); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java b/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java index f3c25cb363..5d50ca1dd4 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java @@ -40,12 +40,8 @@ public static

ExpressionList

forFetchGroup(Query

q) { /** * Construct for Text root expression list - this handles implicit Bool Should, Must etc. */ - public DefaultExpressionList(Query query) { - this(query, query.getExpressionFactory(), null, new ArrayList<>(), true); - } - - public DefaultExpressionList(Query query, ExpressionList parentExprList) { - this(query, query.getExpressionFactory(), parentExprList, new ArrayList<>()); + public DefaultExpressionList(Query query, boolean textRoot) { + this(query, query.getExpressionFactory(), null, new ArrayList<>(), textRoot); } DefaultExpressionList(Query query, ExpressionFactory expr, ExpressionList parentExprList, List list) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java b/ebean-core/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java index 77e1dc879a..2861237a27 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java @@ -24,9 +24,9 @@ final class JunctionExpression implements SpiJunction, SpiExpression, Expr DefaultExpressionList exprList; Junction.Type type; - JunctionExpression(Junction.Type type, Query query, ExpressionList parent) { + JunctionExpression(Junction.Type type, Query query, ExpressionFactory expr, ExpressionList parent) { this.type = type; - this.exprList = new DefaultExpressionList<>(query, parent); + this.exprList = new DefaultExpressionList<>(query, expr, parent, new ArrayList<>()); } /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java index 3f34efa219..a70c6547ce 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -1895,7 +1895,7 @@ public final Query where(Expression expression) { public final ExpressionList text() { if (textExpressions == null) { useDocStore = true; - textExpressions = new DefaultExpressionList<>(this); + textExpressions = new DefaultExpressionList<>(this, true); } return textExpressions; } @@ -1903,7 +1903,7 @@ public final ExpressionList text() { @Override public final ExpressionList where() { if (whereExpressions == null) { - whereExpressions = new DefaultExpressionList<>(this, null); + whereExpressions = new DefaultExpressionList<>(this, false); } return whereExpressions; } @@ -1924,7 +1924,7 @@ public final Query having(Expression expression) { @Override public final ExpressionList having() { if (havingExpressions == null) { - havingExpressions = new DefaultExpressionList<>(this, null); + havingExpressions = new DefaultExpressionList<>(this, false); } return havingExpressions; } diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/QueryBean.java b/ebean-querybean/src/main/java/io/ebean/typequery/QueryBean.java index 1bb177edb3..2758570601 100644 --- a/ebean-querybean/src/main/java/io/ebean/typequery/QueryBean.java +++ b/ebean-querybean/src/main/java/io/ebean/typequery/QueryBean.java @@ -121,7 +121,7 @@ protected QueryBean(boolean aliasDummy) { /** Construct for FilterMany */ protected QueryBean(ExpressionList filter) { this.query = null; - this.root = null; + this.root = (R) this; this.whereStack = new ArrayStack<>(); whereStack.push(filter); } diff --git a/ebean-querybean/src/test/java/org/querytest/QCustomerTest.java b/ebean-querybean/src/test/java/org/querytest/QCustomerTest.java index 94bc120981..cc17a43653 100644 --- a/ebean-querybean/src/test/java/org/querytest/QCustomerTest.java +++ b/ebean-querybean/src/test/java/org/querytest/QCustomerTest.java @@ -377,6 +377,20 @@ void filterManySingleQuery() { assertThat(q.getGeneratedSql()).contains(" from be_customer t0 left join be_contact t1 on t1.customer_id = t0.id where (t1.id is null or (t1.first_name like ? and t1.first_name like ? escape'|')) order by t0.id"); } + @Test + void filterManyOr() { + var q = new QCustomer() + .contacts.filterMany(c -> + c.or() + .firstName.startsWith("R") + .lastName.startsWith("R") + .endOr()) + .query(); + + q.findList(); + assertThat(q.getGeneratedSql()).contains(" from be_customer t0 left join be_contact t1 on t1.customer_id = t0.id where (t1.id is null or ((t1.first_name like ? escape'|' or t1.last_name like ? escape'|'))) order by t0.id"); + } + @Test public void testIdIn() {