diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Criteria.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Criteria.java index 10626797..39c1ebec 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Criteria.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Criteria.java @@ -18,9 +18,13 @@ package org.springframework.data.relational.core.query; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.StringJoiner; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.util.Pair; import org.springframework.lang.Nullable; @@ -370,6 +374,128 @@ public class Criteria implements CriteriaDefinition { return ignoreCase; } + @Override + public String toString() { + + if (isEmpty()) { + return ""; + } + + StringBuilder builder = new StringBuilder(); + unroll(this, builder); + + return builder.toString(); + } + + private void unroll(CriteriaDefinition criteria, StringBuilder stringBuilder) { + + CriteriaDefinition current = criteria; + + // reverse unroll criteria chain + Map forwardChain = new HashMap<>(); + + while (current.hasPrevious()) { + forwardChain.put(current.getPrevious(), current); + current = current.getPrevious(); + } + + // perform the actual mapping + render(current, stringBuilder); + while (forwardChain.containsKey(current)) { + + CriteriaDefinition criterion = forwardChain.get(current); + + if (criterion.getCombinator() != Combinator.INITIAL) { + stringBuilder.append(' ').append(criterion.getCombinator().name()).append(' '); + } + + render(criterion, stringBuilder); + + current = criterion; + } + } + + private void unrollGroup(List criteria, StringBuilder stringBuilder) { + + stringBuilder.append("("); + + boolean first = true; + for (CriteriaDefinition criterion : criteria) { + + if (criterion.isEmpty()) { + continue; + } + + if (!first) { + Combinator combinator = criterion.getCombinator() == Combinator.INITIAL ? Combinator.AND + : criterion.getCombinator(); + stringBuilder.append(' ').append(combinator.name()).append(' '); + } + + unroll(criterion, stringBuilder); + first = false; + } + + stringBuilder.append(")"); + } + + private void render(CriteriaDefinition criteria, StringBuilder stringBuilder) { + + if (criteria.isEmpty()) { + return; + } + + if (criteria.isGroup()) { + unrollGroup(criteria.getGroup(), stringBuilder); + return; + } + + stringBuilder.append(criteria.getColumn().toSql(IdentifierProcessing.NONE)).append(' ') + .append(criteria.getComparator().getComparator()); + + switch (criteria.getComparator()) { + case BETWEEN: + case NOT_BETWEEN: + Pair pair = (Pair) criteria.getValue(); + stringBuilder.append(' ').append(pair.getFirst()).append(" AND ").append(pair.getSecond()); + break; + + case IS_NULL: + case IS_NOT_NULL: + case IS_TRUE: + case IS_FALSE: + break; + + case IN: + case NOT_IN: + stringBuilder.append(" (").append(renderValue(criteria.getValue())).append(')'); + break; + + default: + stringBuilder.append(' ').append(renderValue(criteria.getValue())); + } + } + + private static String renderValue(@Nullable Object value) { + + if (value instanceof Number) { + return value.toString(); + } + + if (value instanceof Collection) { + + StringJoiner joiner = new StringJoiner(", "); + ((Collection) value).forEach(o -> joiner.add(renderValue(o))); + return joiner.toString(); + } + + if (value != null) { + return String.format("'%s'", value); + } + + return "null"; + } + /** * Interface declaring terminal builder methods to build a {@link Criteria}. */ diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/CriteriaDefinition.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/CriteriaDefinition.java index 8b922f35..a0c33750 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/CriteriaDefinition.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/CriteriaDefinition.java @@ -135,6 +135,18 @@ public interface CriteriaDefinition { } enum Comparator { - INITIAL, EQ, NEQ, BETWEEN, NOT_BETWEEN, LT, LTE, GT, GTE, IS_NULL, IS_NOT_NULL, LIKE, NOT_LIKE, NOT_IN, IN, IS_TRUE, IS_FALSE + INITIAL(""), EQ("="), NEQ("!="), BETWEEN("BETWEEN"), NOT_BETWEEN("NOT BETWEEN"), LT("<"), LTE("<="), GT(">"), GTE( + ">="), IS_NULL("IS NULL"), IS_NOT_NULL("IS NOT NULL"), LIKE( + "LIKE"), NOT_LIKE("NOT LIKE"), NOT_IN("NOT IN"), IN("IN"), IS_TRUE("IS TRUE"), IS_FALSE("IS FALSE"); + + private final String comparator; + + Comparator(String comparator) { + this.comparator = comparator; + } + + public String getComparator() { + return comparator; + } } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Update.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Update.java index aac9cb2a..bf13e81d 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Update.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Update.java @@ -18,7 +18,9 @@ package org.springframework.data.relational.core.query; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.StringJoiner; +import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -105,4 +107,21 @@ public class Update { return new Update(updates); } + + @Override + public String toString() { + + if (getAssignments().isEmpty()) { + return ""; + } + + StringJoiner joiner = new StringJoiner(", "); + + getAssignments().forEach((column, o) -> { + joiner.add( + String.format("%s = %s", column.toSql(IdentifierProcessing.NONE), o instanceof Number ? o : "'" + o + "'")); + }); + + return "SET " + joiner.toString(); + } } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/query/CriteriaUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/query/CriteriaUnitTests.java index 3fd4fe05..282156fc 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/query/CriteriaUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/query/CriteriaUnitTests.java @@ -44,6 +44,7 @@ public class CriteriaUnitTests { assertThat(criteria.isGroup()).isTrue(); assertThat(criteria.getGroup()).containsExactly(nested1, nested2); assertThat(criteria.getPrevious()).isEqualTo(Criteria.empty()); + assertThat(criteria).hasToString("(foo IS NOT NULL AND foo IS NULL)"); } @Test // DATAJDBC-513 @@ -52,7 +53,7 @@ public class CriteriaUnitTests { Criteria nested = where("foo").is("bar").and("baz").isNotNull(); Criteria criteria = Criteria.from(nested); - assertThat(criteria).isSameAs(nested); + assertThat(criteria).isSameAs(nested).hasToString("foo = 'bar' AND baz IS NOT NULL"); } @Test // DATAJDBC-513 @@ -98,11 +99,12 @@ public class CriteriaUnitTests { @Test // DATAJDBC-513 public void andGroupedCriteria() { - Criteria criteria = where("foo").is("bar").and(where("foo").is("baz")); + Criteria grouped = where("foo").is("bar").and(where("foo").is("baz").or("bar").isNotNull()); + Criteria criteria = grouped; assertThat(criteria.isGroup()).isTrue(); assertThat(criteria.getGroup()).hasSize(1); - assertThat(criteria.getGroup().get(0).getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getGroup().get(0).getColumn()).isEqualTo(SqlIdentifier.unquoted("bar")); assertThat(criteria.getCombinator()).isEqualTo(Criteria.Combinator.AND); criteria = criteria.getPrevious(); @@ -111,6 +113,8 @@ public class CriteriaUnitTests { assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.EQ); assertThat(criteria.getValue()).isEqualTo("bar"); + + assertThat(grouped).hasToString("foo = 'bar' AND (foo = 'baz' OR bar IS NOT NULL)"); } @Test // DATAJDBC-513 @@ -184,6 +188,7 @@ public class CriteriaUnitTests { assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.IN); assertThat(criteria.getValue()).isEqualTo(Arrays.asList("bar", "baz")); + assertThat(criteria).hasToString("foo IN ('bar', 'baz')"); } @Test // DATAJDBC-513 diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/query/UpdateUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/query/UpdateUnitTests.java new file mode 100644 index 00000000..64068ba8 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/query/UpdateUnitTests.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.query; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.*; + +/** + * Unit tests for {@link Update}. + * + * @author Mark Paluch + */ +public class UpdateUnitTests { + + @Test // DATAJDBC-513 + public void shouldRenderUpdateToString() { + + assertThat(Update.update("foo", "baz").set("bar", 42)).hasToString("SET foo = 'baz', bar = 42"); + } +}