From ea60e42a4d597a67f5319ac2af751ee16fe42cd0 Mon Sep 17 00:00:00 2001 From: rob bygrave Date: Thu, 7 Feb 2019 23:29:42 +1300 Subject: [PATCH] #1633 - ENH: Add bind parameter expansion for raw() expressions --- .../expression/DefaultExpressionFactory.java | 4 +- .../server/expression/RawExpression.java | 4 +- .../expression/RawExpressionBuilder.java | 86 ++++++++++++++++++ .../expression/RawExpressionBuilderTest.java | 90 +++++++++++++++++++ .../org/tests/query/TestWhereRawClause.java | 39 ++++++-- 5 files changed, 212 insertions(+), 11 deletions(-) create mode 100644 src/main/java/io/ebeaninternal/server/expression/RawExpressionBuilder.java create mode 100644 src/test/java/io/ebeaninternal/server/expression/RawExpressionBuilderTest.java diff --git a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionFactory.java b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionFactory.java index ba845529d..d7a1d9e36 100644 --- a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionFactory.java +++ b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionFactory.java @@ -529,7 +529,7 @@ public class DefaultExpressionFactory implements SpiExpressionFactory { */ @Override public Expression raw(String raw, Object value) { - return new RawExpression(raw, new Object[]{value}); + return RawExpressionBuilder.buildSingle(raw, value); } /** @@ -541,7 +541,7 @@ public class DefaultExpressionFactory implements SpiExpressionFactory { */ @Override public Expression raw(String raw, Object[] values) { - return new RawExpression(raw, values); + return RawExpressionBuilder.build(raw, values); } /** diff --git a/src/main/java/io/ebeaninternal/server/expression/RawExpression.java b/src/main/java/io/ebeaninternal/server/expression/RawExpression.java index cad8582bd..295ddc75b 100644 --- a/src/main/java/io/ebeaninternal/server/expression/RawExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/RawExpression.java @@ -12,9 +12,9 @@ import java.util.Collection; class RawExpression extends NonPrepareExpression { - private final String sql; + final String sql; - private final Object[] values; + final Object[] values; RawExpression(String sql, Object[] values) { this.sql = sql; diff --git a/src/main/java/io/ebeaninternal/server/expression/RawExpressionBuilder.java b/src/main/java/io/ebeaninternal/server/expression/RawExpressionBuilder.java new file mode 100644 index 000000000..73a4da911 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/expression/RawExpressionBuilder.java @@ -0,0 +1,86 @@ +package io.ebeaninternal.server.expression; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +class RawExpressionBuilder { + + private static final String BP_1 = "?1"; + + private static final String[] BP = {BP_1, "?2", "?3", "?4", "?5", "?6", "?7", "?8", + "?9", "?10", "?11", "?12", "?13", "?14", "?15", "?16", "?17", "?18", "?19", "?20"}; + + /** + * Build RawExpression taking into account parameter expansion. + */ + static RawExpression buildSingle(String raw, Object value) { + if (isExpand(value, raw, BP_1)) { + Collection val = (Collection) value; + raw = raw.replace(BP_1, expand(val)); + return new RawExpression(raw, val.toArray()); + } + return new RawExpression(raw, new Object[]{value}); + } + + /** + * Build RawExpression for multiple bind values taking into account parameter expansion. + */ + static RawExpression build(String raw, Object[] values) { + + for (int i = 0; i < values.length; i++) { + if (isExpand(values[i], raw, match(i))) { + return new RawExpressionBuilder(raw, values).build(); + } + } + return new RawExpression(raw, values); + } + + /** + * Return true if this value is a collection that should be expanded. + */ + private static boolean isExpand(Object value, String raw, String bindMatch) { + return value instanceof Collection && raw.contains(bindMatch); + } + + private static String match(int i) { + if (i < 20) { + return BP[i]; + } + return "?" + (i + 1); + } + + private static String expand(Collection values) { + + StringBuilder sqlExpand = new StringBuilder(values.size() * 2); + for (int i = 0; i < values.size(); i++) { + if (i > 0) { + sqlExpand.append(","); + } + sqlExpand.append("?"); + } + return sqlExpand.toString(); + } + + private final String expanded; + + private final List params = new ArrayList<>(); + + private RawExpressionBuilder(String raw, Object[] values) { + for (int i = 0; i < values.length; i++) { + String match = match(i); + if (!isExpand(values[i], raw, match)) { + params.add(values[i]); + } else { + Collection val = (Collection) values[i]; + params.addAll(val); + raw = raw.replace(match, expand(val)); + } + } + this.expanded = raw; + } + + private RawExpression build() { + return new RawExpression(expanded, params.toArray()); + } +} diff --git a/src/test/java/io/ebeaninternal/server/expression/RawExpressionBuilderTest.java b/src/test/java/io/ebeaninternal/server/expression/RawExpressionBuilderTest.java new file mode 100644 index 000000000..8f3c4bf43 --- /dev/null +++ b/src/test/java/io/ebeaninternal/server/expression/RawExpressionBuilderTest.java @@ -0,0 +1,90 @@ +package io.ebeaninternal.server.expression; + +import org.junit.Test; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +public class RawExpressionBuilderTest { + + @Test + public void buildSingle_noop() { + + RawExpression exp = RawExpressionBuilder.buildSingle("foo = ?", 42); + assertThat(exp.sql).isEqualTo("foo = ?"); + } + + @Test + public void buildSingle_noExpand() { + + RawExpression exp = RawExpressionBuilder.buildSingle("foo = ?", asList(42, 43)); + assertThat(exp.sql).isEqualTo("foo = ?"); + } + + @Test + public void buildSingle_expand() { + + RawExpression exp = RawExpressionBuilder.buildSingle("foo in (?1)", asList(42, 43)); + assertThat(exp.sql).isEqualTo("foo in (?,?)"); + assertThat(exp.values).contains(42, 43); + } + + @Test + public void buildSingle_expand_more() { + + RawExpression exp = RawExpressionBuilder.buildSingle("foo in (?1)", asList(42, 43, 44, 45)); + assertThat(exp.sql).isEqualTo("foo in (?,?,?,?)"); + assertThat(exp.values).contains(42, 43, 44, 45); + } + + + @Test + public void buildSingle_expand_single() { + + RawExpression exp = RawExpressionBuilder.buildSingle("foo in (?1)", asList(42)); + assertThat(exp.sql).isEqualTo("foo in (?)"); + assertThat(exp.values).contains(42); + } + + + @Test + public void build_noop() { + RawExpression exp = RawExpressionBuilder.build("foo = ?", asArray(42)); + assertThat(exp.sql).isEqualTo("foo = ?"); + } + + @Test + public void build_noExpand() { + + RawExpression exp = RawExpressionBuilder.buildSingle("foo = ? and bar = any(?)", asArray(44, asList(42, 43))); + assertThat(exp.sql).isEqualTo("foo = ? and bar = any(?)"); + } + + @Test + public void build_expand() { + + RawExpression exp = RawExpressionBuilder.build("foo in (?) and bar in (?2)", asArray(44, asList(42, 43))); + assertThat(exp.sql).isEqualTo("foo in (?) and bar in (?,?)"); + assertThat(exp.values).contains(44, 42, 43); + } + + @Test + public void build_expand2() { + + RawExpression exp = RawExpressionBuilder.build("foo in (?) and bar in (?2) (?3)", asArray(44, asList(42, 43), asList(91, 92, 93))); + assertThat(exp.sql).isEqualTo("foo in (?) and bar in (?,?) (?,?,?)"); + assertThat(exp.values).containsExactly(44, 42, 43, 91, 92, 93); + } + + @Test + public void build_expand3() { + + RawExpression exp = RawExpressionBuilder.build("foo in (?) and bar in (?2) (?3) and ?", asArray(44, asList(42, 43), asList(91, 92, 93), 87)); + assertThat(exp.sql).isEqualTo("foo in (?) and bar in (?,?) (?,?,?) and ?"); + assertThat(exp.values).containsExactly(44, 42, 43, 91, 92, 93, 87); + } + + private Object[] asArray(Object... values) { + return values; + } +} diff --git a/src/test/java/org/tests/query/TestWhereRawClause.java b/src/test/java/org/tests/query/TestWhereRawClause.java index 9293a0006..a84ffdfd6 100644 --- a/src/test/java/org/tests/query/TestWhereRawClause.java +++ b/src/test/java/org/tests/query/TestWhereRawClause.java @@ -6,16 +6,16 @@ import io.ebean.Expr; import io.ebean.Query; import io.ebean.annotation.ForPlatform; import io.ebean.annotation.Platform; +import org.junit.Test; import org.tests.model.basic.Customer; import org.tests.model.basic.Order; import org.tests.model.basic.OrderDetail; import org.tests.model.basic.ResetBasicData; -import org.junit.Test; import java.sql.Timestamp; -import java.util.ArrayList; import java.util.List; +import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; public class TestWhereRawClause extends BaseTestCase { @@ -50,6 +50,35 @@ public class TestWhereRawClause extends BaseTestCase { } + @Test + public void testRaw_bindExpansion_subquery() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Customer.class) + .where() + .raw("id in (select c.id from o_customer c where c.name in (?1))", asList("Rob", "Fiona", "Jack")) + .query(); + + List list = query.findList(); + assertThat(list).isNotEmpty(); + assertThat(sqlOf(query)).contains(" t0.id in (select c.id from o_customer c where c.name in (?,?,?))"); + } + + @Test + public void testRaw_bindExpansion() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Customer.class) + .where() + .raw("name in (?1)", asList("Rob", "Fiona", "Jack")) + .query(); + + List list = query.findList(); + assertThat(list).isNotEmpty(); + assertThat(sqlOf(query)).contains(" t0.name in (?,?,?)"); + } @Test @ForPlatform(Platform.POSTGRES) @@ -57,13 +86,9 @@ public class TestWhereRawClause extends BaseTestCase { ResetBasicData.reset(); - List names = new ArrayList<>(); - names.add("Rob"); - names.add("Fiona"); - List list = Ebean.find(Customer.class) .where() - .raw("name = any(?)", names) + .raw("name = any(?)", asList("Rob", "Fiona", "Jack")) .findList(); assertThat(list).isNotEmpty();