#2813 - Sometimes subquery use wrong alias in SQL

The reason for this is that as part of DefaultOrmQuery.copy() it uses
DefaultExpressionList.copy() and that assumed that expressions were
safe to share which is NOT the case for IN and EXISTS sub-query expressions
so InQueryExpression and ExistsQueryExpression

The effective fix for this is that DefaultExpressionList.copy() changes
to call SpiExpression.copy() and for InQueryExpression and ExistsQueryExpression
to implement that copy() by creating a copy of the sub-query.

A "side-fix" is that in DefaultOrmQuery.createExtraJoinsToSupportManyWhereClause()
it was creating an instance of ManyWhereJoins, then mutating it ... and if we
change that to only doing the assignment at the end (object assignment is atomic)
then racy access reading ManyWhereJoins would always get a fully completed non-mutating
instance of ManyWhereJoins. Noting this because it kind of points to where I think
the race condition is (in createExtraJoinsToSupportManyWhereClause()) but noting that
with the change to DefaultExpressionList.copy() this "side-fix" isn't required per say.
This commit is contained in:
Rob Bygrave
2022-08-31 17:24:55 +12:00
parent f07c7411fa
commit 29e2b81092
12 changed files with 110 additions and 28 deletions
@@ -50,7 +50,6 @@ public final class ManyWhereJoins implements Serializable {
* Add a many where join.
*/
public void add(ElPropertyDeploy elProp) {
String join = elProp.elPrefix();
BeanProperty p = elProp.beanProperty();
if (p instanceof BeanPropertyAssocMany<?>) {
@@ -111,7 +110,6 @@ public final class ManyWhereJoins implements Serializable {
* Return the set of property names for the many where joins.
*/
public TreeSet<String> getPropertyNames() {
TreeSet<String> propertyNames = new TreeSet<>();
for (PropertyJoin join : joins.values()) {
propertyNames.add(join.getProperty());
@@ -110,4 +110,11 @@ public interface SpiExpression extends Expression {
* Apply property prefix when filterMany expressions included into main query.
*/
void prefixProperty(String path);
/**
* Return a copy of the expression (as part of creating a query copy).
*/
default SpiExpression copy() {
return this;
}
}
@@ -76,11 +76,7 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
}
public DefaultExpressionList(Query<T> query, ExpressionList<T> parentExprList) {
this(query, query.getExpressionFactory(), parentExprList);
}
DefaultExpressionList(Query<T> query, ExpressionFactory expr, ExpressionList<T> parentExprList) {
this(query, expr, parentExprList, new ArrayList<>());
this(query, query.getExpressionFactory(), parentExprList, new ArrayList<>());
}
DefaultExpressionList(Query<T> query, ExpressionFactory expr, ExpressionList<T> parentExprList, List<SpiExpression> list) {
@@ -243,13 +239,12 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
/**
* Return a copy of the expression list.
* <p>
* Each of the expressions are expected to be immutable and safe to reference.
* </p>
*/
public DefaultExpressionList<T> copy(Query<T> query) {
DefaultExpressionList<T> copy = new DefaultExpressionList<>(query, expr, null);
copy.list.addAll(list);
DefaultExpressionList<T> copy = new DefaultExpressionList<>(query, expr, null, new ArrayList<>(list.size()));
for (SpiExpression expr : list) {
copy.list.add(expr.copy());
}
return copy;
}
@@ -27,6 +27,11 @@ final class ExistsQueryExpression implements SpiExpression, UnsupportedDocStoreE
this.subQuery = null;
}
@Override
public SpiExpression copy() {
return subQuery == null ? this : new ExistsQueryExpression(subQuery.copy(), not);
}
@Override
public void prefixProperty(String path) {
// do nothing
@@ -27,7 +27,7 @@ public final class FilterExpressionList<T> extends DefaultExpressionList<T> {
}
public FilterExpressionList(FilterExprPath pathPrefix, ExpressionFactory expr, Query<T> rootQuery) {
super(null, expr, null);
super(null, expr, null, new ArrayList<>());
this.pathPrefix = pathPrefix;
this.rootQuery = rootQuery;
}
@@ -31,6 +31,11 @@ final class InQueryExpression extends AbstractExpression implements UnsupportedD
this.bindParams = bindParams;
}
@Override
public SpiExpression copy() {
return subQuery == null ? this : new InQueryExpression(propName, subQuery.copy(), not);
}
@Override
public void simplify() {
// do nothing
@@ -43,7 +48,6 @@ final class InQueryExpression extends AbstractExpression implements UnsupportedD
@Override
public void prepareExpression(BeanQueryRequest<?> request) {
CQuery<?> subQuery = compileSubQuery(request);
this.bindParams = subQuery.predicates().whereExprBindValues();
this.sql = subQuery.generatedSql().replace('\n', ' ');
@@ -71,7 +75,6 @@ final class InQueryExpression extends AbstractExpression implements UnsupportedD
@Override
public void addSql(SpiExpressionRequest request) {
request.append(" (").append(propName).append(")");
if (not) {
request.append(" not");
@@ -83,7 +86,6 @@ final class InQueryExpression extends AbstractExpression implements UnsupportedD
@Override
public void addBindValues(SpiExpressionRequest request) {
for (Object bindParam : bindParams) {
request.addBindValue(bindParam);
}
@@ -412,21 +412,22 @@ public class DefaultOrmQuery<T> extends AbstractQuery implements SpiQuery<T> {
}
private void createExtraJoinsToSupportManyWhereClause() {
manyWhereJoins = new ManyWhereJoins();
final var manyWhere = new ManyWhereJoins();
if (whereExpressions != null) {
whereExpressions.containsMany(beanDescriptor, manyWhereJoins);
whereExpressions.containsMany(beanDescriptor, manyWhere);
}
if (havingExpressions != null) {
havingExpressions.containsMany(beanDescriptor, manyWhereJoins);
havingExpressions.containsMany(beanDescriptor, manyWhere);
}
if (orderBy != null) {
for (Property orderProperty : orderBy.getProperties()) {
ElPropertyDeploy elProp = beanDescriptor.elPropertyDeploy(orderProperty.getProperty());
if (elProp != null && elProp.containsFormulaWithJoin()) {
manyWhereJoins.addFormulaWithJoin(elProp.elPrefix(), elProp.name());
manyWhere.addFormulaWithJoin(elProp.elPrefix(), elProp.name());
}
}
}
manyWhereJoins = manyWhere;
}
/**
@@ -1,16 +1,20 @@
package io.ebeaninternal.server.expression;
import io.ebean.ExpressionList;
import io.ebean.Query;
import io.ebeaninternal.api.SpiQuery;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
public class DefaultExpressionListTest extends BaseExpressionTest {
DefaultExpressionList<?> exp() {
return new DefaultExpressionList<>(null, new DefaultExpressionFactory(true, true), null);
return new DefaultExpressionList<>(null, new DefaultExpressionFactory(true, true), null, new ArrayList<>());
}
private <T> DefaultExpressionList<T> spi(ExpressionList<T> list) {
@@ -95,4 +99,34 @@ public class DefaultExpressionListTest extends BaseExpressionTest {
.isSameByBind(spi(exp().eq("a", 10).eq("b", 20)))).isFalse();
}
@SuppressWarnings("unchecked")
@Test
void copy() {
DefaultExpressionList<?> orig = exp();
orig.eq("a", 10).in("b", 11);
DefaultExpressionList<?> copy = (DefaultExpressionList<?>)orig.copy(mock(Query.class));
assertThat(copy).isNotSameAs(orig);
assertThat(copy.list).hasSize(2);
assertThat(copy.list.get(0)).isSameAs(orig.list.get(0));
assertThat(copy.list.get(1)).isSameAs(orig.list.get(1));
}
@SuppressWarnings("unchecked")
@Test
void copy_withSubQuery() {
DefaultExpressionList<?> orig = exp();
SpiQuery<?> inSubQuery = mock(SpiQuery.class);
SpiQuery<?> existsSubQuery = mock(SpiQuery.class);
orig.eq("a", 10).in("name", inSubQuery).exists(existsSubQuery);
DefaultExpressionList<?> copy = (DefaultExpressionList<?>)orig.copy(mock(Query.class));
assertThat(copy.list).hasSize(3);
assertThat(copy.list.get(0)).isSameAs(orig.list.get(0));
assertThat(copy.list.get(1)).isNotSameAs(orig.list.get(1));
assertThat(copy.list.get(2)).isNotSameAs(orig.list.get(2));
verify(inSubQuery).copy();
verify(existsSubQuery).copy();
}
}
@@ -1,10 +1,14 @@
package io.ebeaninternal.server.expression;
import io.ebeaninternal.api.SpiExpression;
import io.ebeaninternal.api.SpiQuery;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
public class ExistsQueryExpressionTest extends BaseExpressionTest {
@@ -13,6 +17,22 @@ public class ExistsQueryExpressionTest extends BaseExpressionTest {
return new ExistsQueryExpression(not, sql, Arrays.asList(bindValues));
}
@Test
void copy_subQuery_expectNewInstance() {
SpiQuery<?> subQuery = mock(SpiQuery.class);
var orig = new ExistsQueryExpression(subQuery, false);
SpiExpression copy = orig.copy();
assertThat(copy).isNotSameAs(orig);
verify(subQuery).copy();
}
@Test
void copy_sqlLiteral_expectSameInstance() {
var orig = exp(true, "sql", 10);
SpiExpression copy = orig.copy();
assertThat(copy).isSameAs(orig);
}
@Test
public void isSameByPlan_when_same() {
@@ -1,18 +1,37 @@
package io.ebeaninternal.server.expression;
import io.ebeaninternal.api.SpiExpression;
import io.ebeaninternal.api.SpiQuery;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
public class InQueryExpressionTest extends BaseExpressionTest {
private InQueryExpression exp(String propertyName, boolean not, String sql, Object... bindValues) {
return new InQueryExpression(propertyName, not, sql, Arrays.asList(bindValues));
}
@Test
void copy_subQuery_expectNewInstance() {
SpiQuery<?> subQuery = mock(SpiQuery.class);
var orig = new InQueryExpression("name", subQuery, false);
SpiExpression copy = orig.copy();
assertThat(copy).isNotSameAs(orig);
verify(subQuery).copy();
}
@Test
void copy_sqlLiteral_expectSameInstance() {
var orig = exp("name", true, "sql", 10);
SpiExpression copy = orig.copy();
assertThat(copy).isSameAs(orig);
}
@Test
public void isSameByPlan_when_same() {
@@ -5,16 +5,16 @@ import io.ebean.Expression;
import io.ebean.Junction;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
public class JunctionExpressionTest extends BaseExpressionTest {
Expression eq(String propName, int value) {
return Expr.eq(propName, value);
}
DefaultExpressionList<?> exp(Expression... expressions) {
DefaultExpressionList<Object> list = new DefaultExpressionList<>(null, new DefaultExpressionFactory(true, false), null);
DefaultExpressionList<Object> list = new DefaultExpressionList<>(null, new DefaultExpressionFactory(true, false), null, new ArrayList<>());
for (Expression ex : expressions) {
list.add(ex);
}
@@ -70,11 +70,12 @@ public class TestQueryFindPagedList extends BaseTestCase {
.setMaxRows(3)
.findPagedList();
Future<Integer> rowCount = pagedList.getFutureCount();
pagedList.loadCount();
List<Order> orders = pagedList.getList();
// these are each getting the total row count
int totalRowCount = pagedList.getTotalCount();
Future<Integer> rowCount = pagedList.getFutureCount();
Integer totalRowCountWithTimeout = rowCount.get(30, TimeUnit.SECONDS);
Integer totalRowCountViaFuture = rowCount.get();