Add InTuples expression for multi-column IN expression

This commit is contained in:
Rob Bygrave
2023-07-03 11:49:53 +12:00
parent d99c50da75
commit e952b555e0
11 changed files with 311 additions and 0 deletions
@@ -0,0 +1,62 @@
package io.ebean;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
*
*/
final class DInTuples implements InTuples {
private final String[] properties;
private final List<Entry> entries = new ArrayList<>();
DInTuples(String[] properties) {
this.properties = properties;
}
/**
*/
@Override
public InTuples add(Object... values) {
entries.add(new DEntry(values));
return this;
}
/**
* Return the first property name.
*/
@Override
public String[] properties() {
return properties;
}
/**
* Return all the value pairs.
*/
@Override
public List<Entry> entries() {
return Collections.unmodifiableList(entries);
}
/**
* A pair of 2 value objects.
* <p>
* Used to support inPairs() expression.
*/
static final class DEntry implements InTuples.Entry {
private final Object[] vals;
DEntry(Object[] vals) {
this.vals = vals;
}
@Override
public Object[] values() {
return vals;
}
}
}
@@ -369,6 +369,11 @@ public interface ExpressionFactory {
*/
Expression inPairs(Pairs pairs);
/**
* In expression using multiple columns.
*/
Expression inTuples(InTuples pairs);
/**
* In - property has a value in the array of values.
*/
@@ -1131,6 +1131,11 @@ public interface ExpressionList<T> {
*/
ExpressionList<T> inPairs(Pairs pairs);
/**
* In expression using multiple columns.
*/
ExpressionList<T> inTuples(InTuples pairs);
/**
* EXISTS a raw SQL SubQuery.
*
@@ -0,0 +1,56 @@
package io.ebean;
import java.util.List;
/**
* IN expression using multiple columns.
* <p>
* Produces SQL expression in the form of (A,B,C) IN ((a0,b0,c0), (a1,b1,c1), ... )
* where A,B,C are the properties in the tuples.
*/
public interface InTuples {
/**
* Create given the properties in the tuples.
*/
static InTuples of(String... properties) {
return new DInTuples(properties);
}
/**
* Create given the properties in the tuples.
*/
static InTuples of(Query.Property<?>... properties) {
String[] props = new String[properties.length];
for (int i = 0; i < properties.length; i++) {
props[i] = properties[i].toString();
}
return new DInTuples(props);
}
/**
* Add a tuple entry.
*/
InTuples add(Object... values);
/**
* Return the properties of the tuples.
*/
String[] properties();
/**
* Return all the tuple entries.
*/
List<Entry> entries();
/**
* A tuple entry.
*/
interface Entry {
/**
* Return all the values for this entry.
*/
Object[] values();
}
}
@@ -471,6 +471,11 @@ public class DefaultExpressionFactory implements SpiExpressionFactory {
return new InPairsExpression(pairs, false);
}
@Override
public Expression inTuples(InTuples pairs) {
return new InTuplesExpression(pairs, false);
}
/**
* In - property has a value in the array of values.
*/
@@ -902,6 +902,10 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
return add(expr.inPairs(pairs));
}
@Override
public ExpressionList<T> inTuples(InTuples pairs) {
return add(expr.inTuples(pairs));
}
@Override
public ExpressionList<T> exists(String sqlSubQuery, Object... bindValues) {
@@ -0,0 +1,112 @@
package io.ebeaninternal.server.expression;
import io.ebean.InTuples;
import io.ebeaninternal.api.BindValuesKey;
import io.ebeaninternal.api.NaturalKeyQueryData;
import io.ebeaninternal.api.SpiExpression;
import io.ebeaninternal.api.SpiExpressionRequest;
import java.util.List;
final class InTuplesExpression extends AbstractExpression {
private final boolean not;
private final String[] properties;
private final List<InTuples.Entry> entries;
InTuplesExpression(InTuples pairs, boolean not) {
super(pairs.properties()[0]);
this.properties = pairs.properties();
// the entries might be modified on cache hit.
this.entries = pairs.entries();
this.not = not;
}
@Override
public boolean naturalKey(NaturalKeyQueryData<?> data) {
return false;
}
@Override
public void writeDocQuery(DocQueryContext context) {
throw new RuntimeException("Not supported with document query");
}
@Override
public void addBindValues(SpiExpressionRequest request) {
for (InTuples.Entry entry : entries) {
for (Object val : entry.values()) {
request.addBindValue(val);
}
}
}
@Override
public void addSql(SpiExpressionRequest request) {
if (entries.isEmpty()) {
request.append(not ? SQL_TRUE : SQL_FALSE);
return;
}
request.append("(");
for (int i = 0; i < properties.length; i++) {
if (i > 0) {
request.append(",");
}
request.property(properties[i]);
}
request.append(") in (");
final String eb = entryBinding();
for (int i = 0; i < entries.size(); i++) {
if (i > 0) {
request.append(",");
}
request.append(eb);
}
request.append(")");
}
private String entryBinding() {
StringBuilder sb = new StringBuilder();
sb.append('(');
for (int i = 0; i < properties.length; i++) {
if (i > 0) {
sb.append(',');
}
sb.append('?');
}
return sb.append(')').toString();
}
/**
* Based on the number of values in the in clause.
*/
@Override
public void queryPlanHash(StringBuilder builder) {
if (not) {
builder.append("Not");
}
builder.append("InTuple[");
for (String property : properties) {
builder.append(property).append("-");
}
builder.append(entries.size()).append("]");
}
@Override
public void queryBindKey(BindValuesKey key) {
key.add(entries.size());
for (InTuples.Entry entry : entries) {
for (Object val : entry.values()) {
key.add(val);
}
}
}
@Override
public boolean isSameByBind(SpiExpression other) {
InTuplesExpression that = (InTuplesExpression) other;
return this.entries.size() == that.entries.size() && entries.equals(that.entries);
}
}
@@ -710,6 +710,11 @@ final class JunctionExpression<T> implements SpiJunction<T>, SpiExpression, Expr
return exprList.inPairs(pairs);
}
@Override
public ExpressionList<T> inTuples(InTuples pairs) {
return exprList.inTuples(pairs);
}
@Override
public ExpressionList<T> in(String propertyName, Collection<?> values) {
return exprList.in(propertyName, values);
@@ -1219,6 +1219,14 @@ public abstract class TQRootBean<T, R> {
return root;
}
/**
* In expression using multiple columns.
*/
public R inTuples(InTuples inTuples) {
peekExprList().inTuples(inTuples);
return root;
}
/**
* Marker that can be used to indicate that the order by clause is defined after this.
* <p>
@@ -1,12 +1,35 @@
package org.querytest;
import io.ebean.InTuples;
import org.example.domain.query.QContact;
import org.junit.jupiter.api.Test;
import java.time.ZonedDateTime;
import static org.assertj.core.api.Assertions.assertThat;
public class QContactTest {
@Test
void findInTuples() {
var c = QContact.alias();
InTuples inTuples = InTuples.of(c.firstName, c.lastName)
.add("Rob", "B")
.add("Bob", "C")
.add("Mob", "D");
var query = new QContact()
.firstName.isNotNull()
.email.isNotNull()
.inTuples(inTuples)
.query();
query.findList();
assertThat(query.getGeneratedSql()).contains("(t0.first_name,t0.last_name) in ((?,?),(?,?),(?,?))");
}
@Test
public void test_oneToManyMap() {
findThem();
@@ -1,5 +1,6 @@
package org.tests.model.basic.cache;
import io.ebean.InTuples;
import io.ebean.xtest.BaseTestCase;
import io.ebean.CacheMode;
import io.ebean.DB;
@@ -419,6 +420,31 @@ public class TestCacheViaComplexNaturalKey3 extends BaseTestCase {
} else {
assertSql(sql.get(0)).contains("from o_cached_natkey3 t0 where t0.store = ? and concat(t0.sku,':',t0.code,'-foo')");
}
}
@Test
public void findList_inTuples_noCache() {
setup();
InTuples tuples = InTuples.of("sku", "code")
.add("2", 1000)
.add("2", 1001)
.add("3", 1000);
LoggedSql.start();
List<OCachedNatKeyBean3> list = DB.find(OCachedNatKeyBean3.class)
.where()
.eq("store", "def")
.inTuples(tuples)
.setUseCache(false)
.orderBy("sku desc")
.findList();
List<String> sql = LoggedSql.stop();
assertThat(tuples.entries()).hasSize(3);
assertThat(list).hasSize(3);
assertSql(sql.get(0)).contains("from o_cached_natkey3 t0 where t0.store = ? and (sku,code) in ((?,?),(?,?),(?,?)) order by t0.sku desc;");
}
}