From abff00666393e3d75f0ff93c9b6c4a180525803d Mon Sep 17 00:00:00 2001 From: mipo256 Date: Fri, 7 Feb 2025 14:09:23 +0300 Subject: [PATCH 1/3] DATAJDBC-1953 Introduced DialectCriteriaCondition Signed-off-by: mipo256 --- .../data/r2dbc/query/CriteriaUnitTests.java | 1 + .../core/dialect/OracleDialect.java | 2 +- .../condition/DialectCriteriaCondition.java | 18 +++ .../core/dialect/condition/Postgres.java | 67 +++++++++ .../data/relational/core/query/Criteria.java | 133 +++++++++++++----- .../core/query/CriteriaDefinition.java | 8 ++ .../core/query/CriteriaUnitTests.java | 70 +++++++++ 7 files changed, 264 insertions(+), 35 deletions(-) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/condition/DialectCriteriaCondition.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/condition/Postgres.java diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/query/CriteriaUnitTests.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/query/CriteriaUnitTests.java index d659c33746..c8a277f62f 100644 --- a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/query/CriteriaUnitTests.java +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/query/CriteriaUnitTests.java @@ -23,6 +23,7 @@ import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; +import org.springframework.data.relational.core.dialect.condition.Postgres; import org.springframework.data.relational.core.query.Criteria; import org.springframework.data.relational.core.sql.SqlIdentifier; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java index 7f65461093..d995673436 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/OracleDialect.java @@ -28,7 +28,7 @@ * An SQL dialect for Oracle. * * @author Jens Schauder - * @author Mikahil Polivakha + * @author Mikhail Polivakha * @since 2.1 */ public class OracleDialect extends AnsiDialect { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/condition/DialectCriteriaCondition.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/condition/DialectCriteriaCondition.java new file mode 100644 index 0000000000..518e6993e4 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/condition/DialectCriteriaCondition.java @@ -0,0 +1,18 @@ +package org.springframework.data.relational.core.dialect.condition; + +import org.springframework.data.relational.core.query.Criteria; + +/** + * This interface represents dialect specific conditions used in WHERE causes built by {@link Criteria}. + * + * @author Mikhail Polivakha + */ +public interface DialectCriteriaCondition { + + /** + * Render a vendor-specific part of the SQL condition. + * + * @return the rendered part of the SQL statement + */ + String render(); +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/condition/Postgres.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/condition/Postgres.java new file mode 100644 index 0000000000..31632f27d6 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/condition/Postgres.java @@ -0,0 +1,67 @@ +package org.springframework.data.relational.core.dialect.condition; + +import org.springframework.data.relational.core.dialect.PostgresDialect; + +/** + * {@link DialectCriteriaCondition DialectCriteriaConditions} that are specific to {@link PostgresDialect PostgreSQL Dialect} + * + * @author Mikhail Polivakha + */ +public class Postgres { + + /** + * Creates a condition that checks that the assumed column of an {@link java.sql.Array} type + * contains an array of any values + * + * @param values array that assumed column should contain + * @return crafted {@link DialectCriteriaCondition} + */ + public static DialectCriteriaCondition arrayContains(Object... values) { + return () -> "@> ARRAY[%s]".formatted(toLiterals(false, values)); + } + + /** + * Creates a condition that checks that the assumed column of an {@link java.sql.Array} type + * contains an array of {@link String} values. + * + * @param values array of {@link String String} that assumed column should contain + * @return crafted {@link DialectCriteriaCondition} + */ + public static DialectCriteriaCondition arrayContains(String... values) { + return () -> "@> ARRAY[%s]::text[]".formatted(toLiterals(true, values)); + } + + /** + * Creates a condition that checks that the assumed column of an {@link java.sql.Array} type + * contains an array of a single {@link String} value. + * + * @param value array of {@link String String} that assumed value should contain + * @return crafted {@link DialectCriteriaCondition} + */ + public static DialectCriteriaCondition arrayContains(String value) { + return arrayContains(new String[]{value}); + } + + @SafeVarargs + private static String toLiterals(boolean quoted, T... values) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < values.length; i++) { + T value = values[i]; + + if (value != null) { + if (quoted) { + result.append('\'').append(value).append('\''); + } else { + result.append(value); + } + } else { + result.append("NULL"); + } + + if (i != values.length - 1) { + result.append(","); + } + } + return result.toString(); + } +} 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 2b2deff2f2..5fdb963ab1 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 @@ -25,6 +25,7 @@ import java.util.StringJoiner; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.relational.core.dialect.condition.DialectCriteriaCondition; import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.util.Pair; @@ -53,11 +54,12 @@ * @author Oliver Drotbohm * @author Roman Chigvintsev * @author Jens Schauder + * @author Mikhail Polivakha * @since 2.0 */ public class Criteria implements CriteriaDefinition { - static final Criteria EMPTY = new Criteria(SqlIdentifier.EMPTY, Comparator.INITIAL, null); + static final Criteria EMPTY = new Criteria(SqlIdentifier.EMPTY, Comparator.INITIAL, null, null); private final @Nullable Criteria previous; private final Combinator combinator; @@ -68,17 +70,25 @@ public class Criteria implements CriteriaDefinition { private final @Nullable Object value; private final boolean ignoreCase; - private Criteria(SqlIdentifier column, Comparator comparator, @Nullable Object value) { - this(null, Combinator.INITIAL, Collections.emptyList(), column, comparator, value, false); + private final DialectCriteriaCondition dialectCriteriaCondition; + + private Criteria(SqlIdentifier column, @Nullable Comparator comparator, @Nullable Object value, DialectCriteriaCondition dialectCriteriaCondition) { + this(null, Combinator.INITIAL, Collections.emptyList(), column, comparator, value, false, dialectCriteriaCondition); } private Criteria(@Nullable Criteria previous, Combinator combinator, List group, - @Nullable SqlIdentifier column, @Nullable Comparator comparator, @Nullable Object value) { - this(previous, combinator, group, column, comparator, value, false); + @Nullable SqlIdentifier column, @Nullable Comparator comparator, @Nullable Object value, DialectCriteriaCondition dialectCriteriaCondition) { + this(previous, combinator, group, column, comparator, value, false, dialectCriteriaCondition); } private Criteria(@Nullable Criteria previous, Combinator combinator, List group, - @Nullable SqlIdentifier column, @Nullable Comparator comparator, @Nullable Object value, boolean ignoreCase) { + @Nullable SqlIdentifier column, @Nullable Comparator comparator, @Nullable Object value, boolean ignoreCase, + @Nullable DialectCriteriaCondition dialectCriteriaCondition) { + + Assert.state( + (dialectCriteriaCondition != null && comparator == null) || (dialectCriteriaCondition == null && comparator != null), + "Either DialectCriteriaCondition or Comparator should be specified for this criteria, but not both" + ); this.previous = previous; this.combinator = previous != null && previous.isEmpty() ? Combinator.INITIAL : combinator; @@ -87,6 +97,7 @@ private Criteria(@Nullable Criteria previous, Combinator combinator, List group) { @@ -98,6 +109,7 @@ private Criteria(@Nullable Criteria previous, Combinator combinator, List criteria) { */ public Criteria ignoreCase(boolean ignoreCase) { if (this.ignoreCase != ignoreCase) { - return new Criteria(previous, combinator, group, column, comparator, value, ignoreCase); + return new Criteria(previous, combinator, group, column, comparator, value, ignoreCase, dialectCriteriaCondition); } return this; } @@ -328,6 +340,7 @@ private boolean doIsEmpty() { /** * @return {@literal true} if this {@link Criteria} is empty. */ + @Override public boolean isGroup() { return !this.group.isEmpty(); } @@ -335,11 +348,17 @@ public boolean isGroup() { /** * @return {@link Combinator} to combine this criteria with a previous one. */ + @Override public Combinator getCombinator() { return combinator; } - @Override + @Override + public DialectCriteriaCondition getDialectCriteriaCondition() { + return dialectCriteriaCondition; + } + + @Override public List getGroup() { return group; } @@ -476,8 +495,16 @@ private void render(CriteriaDefinition criteria, StringBuilder stringBuilder) { return; } - stringBuilder.append(criteria.getColumn().toSql(IdentifierProcessing.NONE)).append(' ') - .append(criteria.getComparator().getComparator()); + stringBuilder.append(criteria.getColumn().toSql(IdentifierProcessing.NONE)).append(' '); + + DialectCriteriaCondition dialectCriteriaCondition = criteria.getDialectCriteriaCondition(); + + if (dialectCriteriaCondition != null) { + stringBuilder.append(dialectCriteriaCondition.render()); + return; + } + + stringBuilder.append(criteria.getComparator().getComparator()); switch (criteria.getComparator()) { case BETWEEN: @@ -653,6 +680,39 @@ public interface CriteriaStep { * @return a new {@link Criteria} object */ Criteria isFalse(); + + /** + * Creates a {@link Criteria} using custom {@link DialectCriteriaCondition}. This API primarily exists + * to handle conditions in WHERE clause that are specific to particular RDBMS vendor. + *

+ * There are some predefined vendor-specific conditions in the {@link org.springframework.data.relational.core.dialect.condition} + * package. For instance, an example of usage is: + *

+ *

+         *    Criteria criteria = Criteria
+         *        .where("tags")
+         *        .satisfies(Postgres.arrayContains("computers", "electronics"))
+         * 
+ * + * This will yield the following SQL: + *

+ *

+         *     tags @> ARRAY['computers','electronics']::text[]
+         * 
+ * + * In the sample above, the assumption is that the 'tags' column is of an 'ARRAY TEXT' or 'ARRAY VARCHAR' PostgreSQL type. + *

+ * If there is no appropriate {@link DialectCriteriaCondition} built-in, please, consider to file an issue, and in the meantime, + * consider to write your own {@link DialectCriteriaCondition}, like this (case insensitive regexp-match in PostgreSQL): + *

+ *

+         *
+         * 
+ * + * @see org.springframework.data.relational.core.dialect.condition.Postgres + * @return a new {@link Criteria} object + */ + Criteria satisfies(DialectCriteriaCondition condition); } /** @@ -671,7 +731,7 @@ public Criteria is(Object value) { Assert.notNull(value, "Value must not be null"); - return createCriteria(Comparator.EQ, value); + return createCriteria(Comparator.EQ, value, null); } @Override @@ -679,7 +739,7 @@ public Criteria not(Object value) { Assert.notNull(value, "Value must not be null"); - return createCriteria(Comparator.NEQ, value); + return createCriteria(Comparator.NEQ, value, null); } @Override @@ -693,7 +753,7 @@ public Criteria in(Object... values) { "You can only pass in one argument of type " + values[1].getClass().getName()); } - return createCriteria(Comparator.IN, Arrays.asList(values)); + return createCriteria(Comparator.IN, Arrays.asList(values), null); } @Override @@ -702,7 +762,7 @@ public Criteria in(Collection values) { Assert.notNull(values, "Values must not be null"); Assert.noNullElements(values.toArray(), "Values must not contain a null value"); - return createCriteria(Comparator.IN, values); + return createCriteria(Comparator.IN, values, null); } @Override @@ -716,7 +776,7 @@ public Criteria notIn(Object... values) { "You can only pass in one argument of type " + values[1].getClass().getName()); } - return createCriteria(Comparator.NOT_IN, Arrays.asList(values)); + return createCriteria(Comparator.NOT_IN, Arrays.asList(values), null); } @Override @@ -725,7 +785,7 @@ public Criteria notIn(Collection values) { Assert.notNull(values, "Values must not be null"); Assert.noNullElements(values.toArray(), "Values must not contain a null value"); - return createCriteria(Comparator.NOT_IN, values); + return createCriteria(Comparator.NOT_IN, values, null); } @Override @@ -734,7 +794,7 @@ public Criteria between(Object begin, Object end) { Assert.notNull(begin, "Begin value must not be null"); Assert.notNull(end, "End value must not be null"); - return createCriteria(Comparator.BETWEEN, Pair.of(begin, end)); + return createCriteria(Comparator.BETWEEN, Pair.of(begin, end), null); } @Override @@ -743,7 +803,7 @@ public Criteria notBetween(Object begin, Object end) { Assert.notNull(begin, "Begin value must not be null"); Assert.notNull(end, "End value must not be null"); - return createCriteria(Comparator.NOT_BETWEEN, Pair.of(begin, end)); + return createCriteria(Comparator.NOT_BETWEEN, Pair.of(begin, end), null); } @Override @@ -751,7 +811,7 @@ public Criteria lessThan(Object value) { Assert.notNull(value, "Value must not be null"); - return createCriteria(Comparator.LT, value); + return createCriteria(Comparator.LT, value, null); } @Override @@ -759,7 +819,7 @@ public Criteria lessThanOrEquals(Object value) { Assert.notNull(value, "Value must not be null"); - return createCriteria(Comparator.LTE, value); + return createCriteria(Comparator.LTE, value, null); } @Override @@ -767,7 +827,7 @@ public Criteria greaterThan(Object value) { Assert.notNull(value, "Value must not be null"); - return createCriteria(Comparator.GT, value); + return createCriteria(Comparator.GT, value, null); } @Override @@ -775,7 +835,7 @@ public Criteria greaterThanOrEquals(Object value) { Assert.notNull(value, "Value must not be null"); - return createCriteria(Comparator.GTE, value); + return createCriteria(Comparator.GTE, value, null); } @Override @@ -783,37 +843,42 @@ public Criteria like(Object value) { Assert.notNull(value, "Value must not be null"); - return createCriteria(Comparator.LIKE, value); + return createCriteria(Comparator.LIKE, value, null); } @Override public Criteria notLike(Object value) { Assert.notNull(value, "Value must not be null"); - return createCriteria(Comparator.NOT_LIKE, value); + return createCriteria(Comparator.NOT_LIKE, value, null); } @Override public Criteria isNull() { - return createCriteria(Comparator.IS_NULL, null); + return createCriteria(Comparator.IS_NULL, null, null); } @Override public Criteria isNotNull() { - return createCriteria(Comparator.IS_NOT_NULL, null); + return createCriteria(Comparator.IS_NOT_NULL, null, null); } @Override public Criteria isTrue() { - return createCriteria(Comparator.IS_TRUE, true); + return createCriteria(Comparator.IS_TRUE, true, null); } @Override public Criteria isFalse() { - return createCriteria(Comparator.IS_FALSE, false); + return createCriteria(Comparator.IS_FALSE, false, null); } - protected Criteria createCriteria(Comparator comparator, @Nullable Object value) { - return new Criteria(this.property, comparator, value); + @Override + public Criteria satisfies(DialectCriteriaCondition condition) { + return createCriteria(null, null, condition); + } + + protected Criteria createCriteria(@Nullable Comparator comparator, @Nullable Object value, DialectCriteriaCondition dialectCriteriaCondition) { + return new Criteria(this.property, comparator, value, dialectCriteriaCondition); } } } 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 c09129a1b6..83c4773311 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 @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.List; +import org.springframework.data.relational.core.dialect.condition.DialectCriteriaCondition; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -28,6 +29,7 @@ * * @author Mark Paluch * @author Jens Schauder + * @author Mikhail Polivakha * @since 2.0 */ public interface CriteriaDefinition { @@ -133,6 +135,12 @@ static CriteriaDefinition from(List criteria) { */ Combinator getCombinator(); + /** + * @return {@link DialectCriteriaCondition} used in this criteria + */ + @Nullable + DialectCriteriaCondition getDialectCriteriaCondition(); + enum Combinator { INITIAL, AND, OR; } 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 d107c67e72..6ad0225df8 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 @@ -22,6 +22,7 @@ import java.util.Arrays; import org.junit.jupiter.api.Test; +import org.springframework.data.relational.core.dialect.condition.Postgres; import org.springframework.data.relational.core.sql.SqlIdentifier; /** @@ -30,6 +31,7 @@ * @author Mark Paluch * @author Jens Schauder * @author Roman Chigvintsev + * @author Mikhail Polivakha */ class CriteriaUnitTests { @@ -95,6 +97,27 @@ void andChainedCriteria() { assertThat(criteria.getValue()).isEqualTo("bar"); } + @Test // DATAJDBC-513 + void andChainedCriteriaWithDialectCriteriaCondition() { + + Criteria criteria = where("foo").is("bar").and("baz").satisfies(Postgres.arrayContains("first", "second")); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("baz")); + assertThat(criteria.getComparator()).isNull(); + assertThat(criteria.getValue()).isNull(); + assertThat(criteria.getPrevious()).isNotNull(); + assertThat(criteria.getDialectCriteriaCondition()).isNotNull(); + assertThat(criteria.getCombinator()).isEqualTo(Criteria.Combinator.AND); + + criteria = criteria.getPrevious(); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.EQ); + assertThat(criteria.getValue()).isEqualTo("bar"); + + criteria.toString().equals("foo = 'bar' AND baz @> ARRAY['first','second']::text[]"); + } + @Test // DATAJDBC-513 void andGroupedCriteria() { @@ -116,6 +139,27 @@ void andGroupedCriteria() { assertThat(grouped).hasToString("foo = 'bar' AND (foo = 'baz' OR bar IS NOT NULL)"); } + @Test // DATAJDBC-1953 + void andGroupedCriteriaWithDialectCriteriaCondition() { + + Criteria grouped = where("foo").is("bar").and(where("foo").is("baz").or("bar").satisfies(Postgres.arrayContains("electronics"))); + Criteria criteria = grouped; + + assertThat(criteria.isGroup()).isTrue(); + assertThat(criteria.getGroup()).hasSize(1); + assertThat(criteria.getGroup().get(0).getColumn()).isEqualTo(SqlIdentifier.unquoted("bar")); + assertThat(criteria.getCombinator()).isEqualTo(Criteria.Combinator.AND); + + criteria = criteria.getPrevious(); + + assertThat(criteria).isNotNull(); + 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 @> ARRAY['electronics']::text[])"); + } + @Test // DATAJDBC-513 void orChainedCriteria() { @@ -179,6 +223,32 @@ void shouldBuildNotEqualsCriteria() { assertThat(criteria.getValue()).isEqualTo("bar"); } + @Test // DATAJDBC-1953 + void shouldBuildSimplePredefinedDialectCriteriaCondition() { + + Criteria criteria = where("foo").satisfies(Postgres.arrayContains(1, 2, 3)); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isNull(); + assertThat(criteria.getDialectCriteriaCondition()).isNotNull(); + assertThat(criteria.getValue()).isNull(); + + assertThat(criteria.toString()).isEqualTo("foo @> ARRAY[1,2,3]"); + } + + @Test // DATAJDBC-1953 + void shouldBuildSimpleCustomDialectCriteriaCondition() { + + Criteria criteria = where("foo").satisfies(() -> "~* '*.example.*'"); + + assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + assertThat(criteria.getComparator()).isNull(); + assertThat(criteria.getDialectCriteriaCondition()).isNotNull(); + assertThat(criteria.getValue()).isNull(); + + assertThat(criteria.toString()).isEqualTo("foo ~* '*.example.*'"); + } + @Test // DATAJDBC-513 void shouldBuildInCriteria() { From 0529b3338b3e20efcc32cfd00296e2fb77e9abc7 Mon Sep 17 00:00:00 2001 From: mipo256 Date: Sun, 6 Apr 2025 16:17:35 +0300 Subject: [PATCH 2/3] GH-1953 removed the DialectCriteraCondition custom string rendering --- .../data/r2dbc/query/CriteriaUnitTests.java | 1 - .../core/dialect/PostgresDialect.java | 1 + .../condition/DialectCriteriaCondition.java | 18 --- .../core/dialect/condition/Postgres.java | 67 --------- .../data/relational/core/query/Criteria.java | 142 ++++++------------ .../core/query/CriteriaDefinition.java | 9 +- .../core/query/OngoingArrayCriteria.java | 34 +++++ .../data/relational/core/query/Postgres.java | 103 +++++++++++++ .../core/query/CriteriaUnitTests.java | 96 ++++++------ 9 files changed, 228 insertions(+), 243 deletions(-) delete mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/condition/DialectCriteriaCondition.java delete mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/condition/Postgres.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/query/OngoingArrayCriteria.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Postgres.java diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/query/CriteriaUnitTests.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/query/CriteriaUnitTests.java index c8a277f62f..d659c33746 100644 --- a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/query/CriteriaUnitTests.java +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/query/CriteriaUnitTests.java @@ -23,7 +23,6 @@ import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; -import org.springframework.data.relational.core.dialect.condition.Postgres; import org.springframework.data.relational.core.query.Criteria; import org.springframework.data.relational.core.sql.SqlIdentifier; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java index 6979c365e9..2bf62bcd76 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java @@ -57,6 +57,7 @@ public class PostgresDialect extends AbstractDialect { private IdentifierProcessing identifierProcessing = IdentifierProcessing.create(Quoting.ANSI, LetterCasing.LOWER_CASE); + private IdGeneration idGeneration = new IdGeneration() { @Override diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/condition/DialectCriteriaCondition.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/condition/DialectCriteriaCondition.java deleted file mode 100644 index 518e6993e4..0000000000 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/condition/DialectCriteriaCondition.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.springframework.data.relational.core.dialect.condition; - -import org.springframework.data.relational.core.query.Criteria; - -/** - * This interface represents dialect specific conditions used in WHERE causes built by {@link Criteria}. - * - * @author Mikhail Polivakha - */ -public interface DialectCriteriaCondition { - - /** - * Render a vendor-specific part of the SQL condition. - * - * @return the rendered part of the SQL statement - */ - String render(); -} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/condition/Postgres.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/condition/Postgres.java deleted file mode 100644 index 31632f27d6..0000000000 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/condition/Postgres.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.springframework.data.relational.core.dialect.condition; - -import org.springframework.data.relational.core.dialect.PostgresDialect; - -/** - * {@link DialectCriteriaCondition DialectCriteriaConditions} that are specific to {@link PostgresDialect PostgreSQL Dialect} - * - * @author Mikhail Polivakha - */ -public class Postgres { - - /** - * Creates a condition that checks that the assumed column of an {@link java.sql.Array} type - * contains an array of any values - * - * @param values array that assumed column should contain - * @return crafted {@link DialectCriteriaCondition} - */ - public static DialectCriteriaCondition arrayContains(Object... values) { - return () -> "@> ARRAY[%s]".formatted(toLiterals(false, values)); - } - - /** - * Creates a condition that checks that the assumed column of an {@link java.sql.Array} type - * contains an array of {@link String} values. - * - * @param values array of {@link String String} that assumed column should contain - * @return crafted {@link DialectCriteriaCondition} - */ - public static DialectCriteriaCondition arrayContains(String... values) { - return () -> "@> ARRAY[%s]::text[]".formatted(toLiterals(true, values)); - } - - /** - * Creates a condition that checks that the assumed column of an {@link java.sql.Array} type - * contains an array of a single {@link String} value. - * - * @param value array of {@link String String} that assumed value should contain - * @return crafted {@link DialectCriteriaCondition} - */ - public static DialectCriteriaCondition arrayContains(String value) { - return arrayContains(new String[]{value}); - } - - @SafeVarargs - private static String toLiterals(boolean quoted, T... values) { - StringBuilder result = new StringBuilder(); - for (int i = 0; i < values.length; i++) { - T value = values[i]; - - if (value != null) { - if (quoted) { - result.append('\'').append(value).append('\''); - } else { - result.append(value); - } - } else { - result.append("NULL"); - } - - if (i != values.length - 1) { - result.append(","); - } - } - return result.toString(); - } -} 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 5fdb963ab1..c5eb852a87 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 @@ -25,7 +25,6 @@ import java.util.StringJoiner; import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.data.relational.core.dialect.condition.DialectCriteriaCondition; import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.util.Pair; @@ -59,7 +58,7 @@ */ public class Criteria implements CriteriaDefinition { - static final Criteria EMPTY = new Criteria(SqlIdentifier.EMPTY, Comparator.INITIAL, null, null); + static final Criteria EMPTY = new Criteria(SqlIdentifier.EMPTY, Comparator.INITIAL, null); private final @Nullable Criteria previous; private final Combinator combinator; @@ -70,25 +69,17 @@ public class Criteria implements CriteriaDefinition { private final @Nullable Object value; private final boolean ignoreCase; - private final DialectCriteriaCondition dialectCriteriaCondition; - - private Criteria(SqlIdentifier column, @Nullable Comparator comparator, @Nullable Object value, DialectCriteriaCondition dialectCriteriaCondition) { - this(null, Combinator.INITIAL, Collections.emptyList(), column, comparator, value, false, dialectCriteriaCondition); + public Criteria(SqlIdentifier column, @Nullable Comparator comparator, @Nullable Object value) { + this(null, Combinator.INITIAL, Collections.emptyList(), column, comparator, value, false); } private Criteria(@Nullable Criteria previous, Combinator combinator, List group, - @Nullable SqlIdentifier column, @Nullable Comparator comparator, @Nullable Object value, DialectCriteriaCondition dialectCriteriaCondition) { - this(previous, combinator, group, column, comparator, value, false, dialectCriteriaCondition); + @Nullable SqlIdentifier column, @Nullable Comparator comparator, @Nullable Object value) { + this(previous, combinator, group, column, comparator, value, false); } private Criteria(@Nullable Criteria previous, Combinator combinator, List group, - @Nullable SqlIdentifier column, @Nullable Comparator comparator, @Nullable Object value, boolean ignoreCase, - @Nullable DialectCriteriaCondition dialectCriteriaCondition) { - - Assert.state( - (dialectCriteriaCondition != null && comparator == null) || (dialectCriteriaCondition == null && comparator != null), - "Either DialectCriteriaCondition or Comparator should be specified for this criteria, but not both" - ); + @Nullable SqlIdentifier column, @Nullable Comparator comparator, @Nullable Object value, boolean ignoreCase) { this.previous = previous; this.combinator = previous != null && previous.isEmpty() ? Combinator.INITIAL : combinator; @@ -97,7 +88,6 @@ private Criteria(@Nullable Criteria previous, Combinator combinator, List group) { @@ -109,7 +99,6 @@ private Criteria(@Nullable Criteria previous, Combinator combinator, List criteria) { */ public Criteria ignoreCase(boolean ignoreCase) { if (this.ignoreCase != ignoreCase) { - return new Criteria(previous, combinator, group, column, comparator, value, ignoreCase, dialectCriteriaCondition); + return new Criteria(previous, combinator, group, column, comparator, value, ignoreCase); } return this; } @@ -340,7 +329,7 @@ private boolean doIsEmpty() { /** * @return {@literal true} if this {@link Criteria} is empty. */ - @Override + @Override public boolean isGroup() { return !this.group.isEmpty(); } @@ -348,17 +337,12 @@ public boolean isGroup() { /** * @return {@link Combinator} to combine this criteria with a previous one. */ - @Override + @Override public Combinator getCombinator() { return combinator; } - @Override - public DialectCriteriaCondition getDialectCriteriaCondition() { - return dialectCriteriaCondition; - } - - @Override + @Override public List getGroup() { return group; } @@ -495,16 +479,8 @@ private void render(CriteriaDefinition criteria, StringBuilder stringBuilder) { return; } - stringBuilder.append(criteria.getColumn().toSql(IdentifierProcessing.NONE)).append(' '); - - DialectCriteriaCondition dialectCriteriaCondition = criteria.getDialectCriteriaCondition(); - - if (dialectCriteriaCondition != null) { - stringBuilder.append(dialectCriteriaCondition.render()); - return; - } - - stringBuilder.append(criteria.getComparator().getComparator()); + stringBuilder.append(criteria.getColumn().toSql(IdentifierProcessing.NONE)).append(' ') + .append(criteria.getComparator().getComparator()); switch (criteria.getComparator()) { case BETWEEN: @@ -542,6 +518,10 @@ private static String renderValue(@Nullable Object value) { return joiner.toString(); } + if (value instanceof CriteriaLiteral literal) { + return literal.getLiteral(); + } + if (value != null) { return String.format("'%s'", value); } @@ -681,38 +661,11 @@ public interface CriteriaStep { */ Criteria isFalse(); - /** - * Creates a {@link Criteria} using custom {@link DialectCriteriaCondition}. This API primarily exists - * to handle conditions in WHERE clause that are specific to particular RDBMS vendor. - *

- * There are some predefined vendor-specific conditions in the {@link org.springframework.data.relational.core.dialect.condition} - * package. For instance, an example of usage is: - *

- *

-         *    Criteria criteria = Criteria
-         *        .where("tags")
-         *        .satisfies(Postgres.arrayContains("computers", "electronics"))
-         * 
- * - * This will yield the following SQL: - *

- *

-         *     tags @> ARRAY['computers','electronics']::text[]
-         * 
- * - * In the sample above, the assumption is that the 'tags' column is of an 'ARRAY TEXT' or 'ARRAY VARCHAR' PostgreSQL type. - *

- * If there is no appropriate {@link DialectCriteriaCondition} built-in, please, consider to file an issue, and in the meantime, - * consider to write your own {@link DialectCriteriaCondition}, like this (case insensitive regexp-match in PostgreSQL): - *

- *

-         *
-         * 
- * - * @see org.springframework.data.relational.core.dialect.condition.Postgres - * @return a new {@link Criteria} object - */ - Criteria satisfies(DialectCriteriaCondition condition); + } + + static interface CriteriaLiteral { + + String getLiteral(); } /** @@ -731,7 +684,7 @@ public Criteria is(Object value) { Assert.notNull(value, "Value must not be null"); - return createCriteria(Comparator.EQ, value, null); + return createCriteria(Comparator.EQ, value); } @Override @@ -739,7 +692,7 @@ public Criteria not(Object value) { Assert.notNull(value, "Value must not be null"); - return createCriteria(Comparator.NEQ, value, null); + return createCriteria(Comparator.NEQ, value); } @Override @@ -753,7 +706,7 @@ public Criteria in(Object... values) { "You can only pass in one argument of type " + values[1].getClass().getName()); } - return createCriteria(Comparator.IN, Arrays.asList(values), null); + return createCriteria(Comparator.IN, Arrays.asList(values)); } @Override @@ -762,7 +715,7 @@ public Criteria in(Collection values) { Assert.notNull(values, "Values must not be null"); Assert.noNullElements(values.toArray(), "Values must not contain a null value"); - return createCriteria(Comparator.IN, values, null); + return createCriteria(Comparator.IN, values); } @Override @@ -776,7 +729,7 @@ public Criteria notIn(Object... values) { "You can only pass in one argument of type " + values[1].getClass().getName()); } - return createCriteria(Comparator.NOT_IN, Arrays.asList(values), null); + return createCriteria(Comparator.NOT_IN, Arrays.asList(values)); } @Override @@ -785,7 +738,7 @@ public Criteria notIn(Collection values) { Assert.notNull(values, "Values must not be null"); Assert.noNullElements(values.toArray(), "Values must not contain a null value"); - return createCriteria(Comparator.NOT_IN, values, null); + return createCriteria(Comparator.NOT_IN, values); } @Override @@ -794,7 +747,7 @@ public Criteria between(Object begin, Object end) { Assert.notNull(begin, "Begin value must not be null"); Assert.notNull(end, "End value must not be null"); - return createCriteria(Comparator.BETWEEN, Pair.of(begin, end), null); + return createCriteria(Comparator.BETWEEN, Pair.of(begin, end)); } @Override @@ -803,7 +756,7 @@ public Criteria notBetween(Object begin, Object end) { Assert.notNull(begin, "Begin value must not be null"); Assert.notNull(end, "End value must not be null"); - return createCriteria(Comparator.NOT_BETWEEN, Pair.of(begin, end), null); + return createCriteria(Comparator.NOT_BETWEEN, Pair.of(begin, end)); } @Override @@ -811,7 +764,7 @@ public Criteria lessThan(Object value) { Assert.notNull(value, "Value must not be null"); - return createCriteria(Comparator.LT, value, null); + return createCriteria(Comparator.LT, value); } @Override @@ -819,7 +772,7 @@ public Criteria lessThanOrEquals(Object value) { Assert.notNull(value, "Value must not be null"); - return createCriteria(Comparator.LTE, value, null); + return createCriteria(Comparator.LTE, value); } @Override @@ -827,7 +780,7 @@ public Criteria greaterThan(Object value) { Assert.notNull(value, "Value must not be null"); - return createCriteria(Comparator.GT, value, null); + return createCriteria(Comparator.GT, value); } @Override @@ -835,7 +788,7 @@ public Criteria greaterThanOrEquals(Object value) { Assert.notNull(value, "Value must not be null"); - return createCriteria(Comparator.GTE, value, null); + return createCriteria(Comparator.GTE, value); } @Override @@ -843,42 +796,37 @@ public Criteria like(Object value) { Assert.notNull(value, "Value must not be null"); - return createCriteria(Comparator.LIKE, value, null); + return createCriteria(Comparator.LIKE, value); } @Override public Criteria notLike(Object value) { Assert.notNull(value, "Value must not be null"); - return createCriteria(Comparator.NOT_LIKE, value, null); + return createCriteria(Comparator.NOT_LIKE, value); } @Override public Criteria isNull() { - return createCriteria(Comparator.IS_NULL, null, null); + return createCriteria(Comparator.IS_NULL, null); } @Override public Criteria isNotNull() { - return createCriteria(Comparator.IS_NOT_NULL, null, null); + return createCriteria(Comparator.IS_NOT_NULL, null); } @Override public Criteria isTrue() { - return createCriteria(Comparator.IS_TRUE, true, null); + return createCriteria(Comparator.IS_TRUE, true); } @Override public Criteria isFalse() { - return createCriteria(Comparator.IS_FALSE, false, null); + return createCriteria(Comparator.IS_FALSE, false); } - @Override - public Criteria satisfies(DialectCriteriaCondition condition) { - return createCriteria(null, null, condition); - } - - protected Criteria createCriteria(@Nullable Comparator comparator, @Nullable Object value, DialectCriteriaCondition dialectCriteriaCondition) { - return new Criteria(this.property, comparator, value, dialectCriteriaCondition); + protected Criteria createCriteria(@Nullable Comparator comparator, @Nullable Object value) { + return new Criteria(this.property, comparator, value); } } } 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 83c4773311..ac69107c31 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 @@ -18,7 +18,6 @@ import java.util.Arrays; import java.util.List; -import org.springframework.data.relational.core.dialect.condition.DialectCriteriaCondition; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -135,12 +134,6 @@ static CriteriaDefinition from(List criteria) { */ Combinator getCombinator(); - /** - * @return {@link DialectCriteriaCondition} used in this criteria - */ - @Nullable - DialectCriteriaCondition getDialectCriteriaCondition(); - enum Combinator { INITIAL, AND, OR; } @@ -148,7 +141,7 @@ enum Combinator { enum Comparator { 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"); + "LIKE"), NOT_LIKE("NOT LIKE"), NOT_IN("NOT IN"), IN("IN"), IS_TRUE("IS TRUE"), IS_FALSE("IS FALSE"), ARRAY_CONTAINS("@>"); private final String comparator; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/OngoingArrayCriteria.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/OngoingArrayCriteria.java new file mode 100644 index 0000000000..4a765fe2b4 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/OngoingArrayCriteria.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020-2025 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; + +/** + * An Ongoing Criteria builder for {@link java.sql.Types#ARRAY SQL Array} related operations. + * Used by intermediate builder objects returned from the {@link Criteria}. + * + * @author Mikhail Polivakha + */ +public interface OngoingArrayCriteria { + + /** + * Builds a {@link Criteria} where the pre-defined array must contain given values. + * + * @param values values to be present in the array + * @return built {@link Criteria} + */ + Criteria contains(Object... values); +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Postgres.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Postgres.java new file mode 100644 index 0000000000..b5b8251b79 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Postgres.java @@ -0,0 +1,103 @@ +/* + * Copyright 2020-2025 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 static org.springframework.data.relational.core.query.Criteria.*; + +import java.sql.JDBCType; +import java.util.StringJoiner; + +import org.jetbrains.annotations.NotNull; +import org.springframework.core.ResolvableType; +import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; + +/** + * PostgreSQL-specific {@link Criteria} conditions. + * + * @author Mikhail Polivakha + */ +public class Postgres { + + /** + * Custom {@link Criteria} condition builder to check if the column of an {@link java.sql.Types#ARRAY ARRAY} sql type + * matches specific conditions. Samples of usage is: + *

+ *

+	 * // Code below produces the SQL: "my_column" @> ARRAY['A', 'B']
+	 * Postgres.whereArray("my_column").contains("A", "B")
+	 * 
+ * Code above produces the SQL: + *
+	 *   "my_column" @> ARRAY['A', 'B']
+	 * 
+ * + * @param arrayName the name of an ARRAY column to match against + * @return the {@link OngoingArrayCriteria} to chain future condition + */ + public static OngoingArrayCriteria array(String arrayName) { + return new PostgresCriteriaArray(arrayName); + } + + public static class PostgresCriteriaArray implements OngoingArrayCriteria { + + private final String arrayColumnName; + + public PostgresCriteriaArray(String arrayColumnName) { + this.arrayColumnName = arrayColumnName; + } + + @NotNull + @Override + public Criteria contains(Object... values) { + Assert.notNull(values, "values array cannot be null"); + + return new Criteria(SqlIdentifier.quoted(arrayColumnName), CriteriaDefinition.Comparator.ARRAY_CONTAINS, new CriteriaLiteral() { + + @Override + public String getLiteral() { + boolean quoted = true; + + if (values.length > 0) { + quoted = !Number.class.isAssignableFrom(values[0].getClass()); + } + + return toArrayLiteral(quoted, values); + } + }); + } + + @SafeVarargs + public final String toArrayLiteral(boolean quoted, T... values) { + StringJoiner accumulator = new StringJoiner(",", "ARRAY[", "]"); + + for (T value : values) { + if (value != null) { + if (quoted) { + accumulator.add("'" + value + "'"); + } else { + accumulator.add(value.toString()); + } + } else { + accumulator.add(JDBCType.NULL.name()); + } + } + return accumulator.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 6ad0225df8..74b16feace 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 @@ -21,8 +21,8 @@ import java.util.Arrays; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; -import org.springframework.data.relational.core.dialect.condition.Postgres; import org.springframework.data.relational.core.sql.SqlIdentifier; /** @@ -100,22 +100,22 @@ void andChainedCriteria() { @Test // DATAJDBC-513 void andChainedCriteriaWithDialectCriteriaCondition() { - Criteria criteria = where("foo").is("bar").and("baz").satisfies(Postgres.arrayContains("first", "second")); - - assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("baz")); - assertThat(criteria.getComparator()).isNull(); - assertThat(criteria.getValue()).isNull(); - assertThat(criteria.getPrevious()).isNotNull(); - assertThat(criteria.getDialectCriteriaCondition()).isNotNull(); - assertThat(criteria.getCombinator()).isEqualTo(Criteria.Combinator.AND); - - criteria = criteria.getPrevious(); - - assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); - assertThat(criteria.getComparator()).isEqualTo(CriteriaDefinition.Comparator.EQ); - assertThat(criteria.getValue()).isEqualTo("bar"); - - criteria.toString().equals("foo = 'bar' AND baz @> ARRAY['first','second']::text[]"); + Criteria criteria = where("foo").is("bar").and(Postgres.array("baz").contains("first", "second")); + var previous = criteria.getPrevious(); + + assertSoftly(softAssertions -> { + softAssertions.assertThat(criteria.getGroup().get(0).getColumn()).isEqualTo(SqlIdentifier.quoted("baz")); + softAssertions.assertThat(criteria.getComparator()).isNull(); + softAssertions.assertThat(criteria.getValue()).isNull(); + softAssertions.assertThat(criteria.getPrevious()).isNotNull(); + softAssertions.assertThat(criteria.getCombinator()).isEqualTo(Criteria.Combinator.AND); + softAssertions.assertThat(criteria.toString()).isEqualTo("foo = 'bar' AND (baz @> ARRAY['first','second'])"); + + softAssertions.assertThat(previous.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + softAssertions.assertThat(previous.getComparator()).isEqualTo(CriteriaDefinition.Comparator.EQ); + softAssertions.assertThat(previous.getValue()).isEqualTo("bar"); + softAssertions.assertThat(previous.toString()).isEqualTo("foo = 'bar'"); + }); } @Test // DATAJDBC-513 @@ -142,22 +142,21 @@ void andGroupedCriteria() { @Test // DATAJDBC-1953 void andGroupedCriteriaWithDialectCriteriaCondition() { - Criteria grouped = where("foo").is("bar").and(where("foo").is("baz").or("bar").satisfies(Postgres.arrayContains("electronics"))); - Criteria criteria = grouped; - - assertThat(criteria.isGroup()).isTrue(); - assertThat(criteria.getGroup()).hasSize(1); - assertThat(criteria.getGroup().get(0).getColumn()).isEqualTo(SqlIdentifier.unquoted("bar")); - assertThat(criteria.getCombinator()).isEqualTo(Criteria.Combinator.AND); + Criteria grouped = where("foo").is("bar").and(where("foo").is("baz").or(Postgres.array("bar").contains("electronics"))); + Criteria previous = grouped.getPrevious(); - criteria = criteria.getPrevious(); + assertSoftly(softAssertions -> { + softAssertions.assertThat(grouped.isGroup()).isTrue(); + softAssertions.assertThat(grouped.getGroup()).hasSize(1); + softAssertions.assertThat(grouped.getGroup().get(0).getGroup().get(0).getColumn()).isEqualTo(SqlIdentifier.unquoted("\"bar\"")); + softAssertions.assertThat(grouped.getCombinator()).isEqualTo(Criteria.Combinator.AND); + softAssertions.assertThat(grouped).hasToString("foo = 'bar' AND (foo = 'baz' OR (bar @> ARRAY['electronics']))"); - assertThat(criteria).isNotNull(); - 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 @> ARRAY['electronics']::text[])"); + softAssertions.assertThat(previous).isNotNull(); + softAssertions.assertThat(previous.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); + softAssertions.assertThat(previous.getComparator()).isEqualTo(CriteriaDefinition.Comparator.EQ); + softAssertions.assertThat(previous.getValue()).isEqualTo("bar"); + }); } @Test // DATAJDBC-513 @@ -226,27 +225,20 @@ void shouldBuildNotEqualsCriteria() { @Test // DATAJDBC-1953 void shouldBuildSimplePredefinedDialectCriteriaCondition() { - Criteria criteria = where("foo").satisfies(Postgres.arrayContains(1, 2, 3)); - - assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); - assertThat(criteria.getComparator()).isNull(); - assertThat(criteria.getDialectCriteriaCondition()).isNotNull(); - assertThat(criteria.getValue()).isNull(); - - assertThat(criteria.toString()).isEqualTo("foo @> ARRAY[1,2,3]"); - } - - @Test // DATAJDBC-1953 - void shouldBuildSimpleCustomDialectCriteriaCondition() { - - Criteria criteria = where("foo").satisfies(() -> "~* '*.example.*'"); - - assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); - assertThat(criteria.getComparator()).isNull(); - assertThat(criteria.getDialectCriteriaCondition()).isNotNull(); - assertThat(criteria.getValue()).isNull(); - - assertThat(criteria.toString()).isEqualTo("foo ~* '*.example.*'"); + Object[] values = { 1, 2, 3 }; + Criteria criteria = Postgres.array("foo").contains(values); + + assertSoftly(softAssertions -> { + softAssertions.assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.quoted("foo")); + softAssertions.assertThat(criteria.getComparator()).isEqualTo(Comparator.ARRAY_CONTAINS); + softAssertions.assertThat(criteria.getValue()).isInstanceOf(CriteriaLiteral.class); + softAssertions + .assertThat(criteria.getValue()) + .asInstanceOf(InstanceOfAssertFactories.type(CriteriaLiteral.class)) + .extracting(CriteriaLiteral::getLiteral) + .isEqualTo("ARRAY[1,2,3]"); + softAssertions.assertThat(criteria.toString()).isEqualTo("foo @> ARRAY[1,2,3]"); + }); } @Test // DATAJDBC-513 From 6b67cc74ba075f3b35d2de037133a8ee852baa63 Mon Sep 17 00:00:00 2001 From: mipo256 Date: Fri, 11 Apr 2025 14:10:57 +0300 Subject: [PATCH 3/3] GH-1953 code review polishing, squash me later Signed-off-by: mipo256 --- .../data/relational/core/query/Criteria.java | 69 ++++++++++++------- .../core/query/CriteriaDefinition.java | 8 ++- .../core/query/ExtendedComparator.java | 43 ++++++++++++ .../data/relational/core/query/Postgres.java | 9 ++- .../core/query/CriteriaUnitTests.java | 2 +- 5 files changed, 99 insertions(+), 32 deletions(-) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/query/ExtendedComparator.java 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 c5eb852a87..d727f8f44b 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 @@ -66,26 +66,32 @@ public class Criteria implements CriteriaDefinition { private final @Nullable SqlIdentifier column; private final @Nullable Comparator comparator; + private final @Nullable ExtendedComparator extendedComparator; private final @Nullable Object value; private final boolean ignoreCase; - public Criteria(SqlIdentifier column, @Nullable Comparator comparator, @Nullable Object value) { - this(null, Combinator.INITIAL, Collections.emptyList(), column, comparator, value, false); + Criteria(SqlIdentifier column, @Nullable Comparator comparator, @Nullable Object value) { + this(null, Combinator.INITIAL, Collections.emptyList(), column, comparator, null, value, false); + } + + Criteria(SqlIdentifier column, ExtendedComparator extendedComparator, @Nullable Object value) { + this(null, Combinator.INITIAL, Collections.emptyList(), column, null, extendedComparator, value, false); } private Criteria(@Nullable Criteria previous, Combinator combinator, List group, @Nullable SqlIdentifier column, @Nullable Comparator comparator, @Nullable Object value) { - this(previous, combinator, group, column, comparator, value, false); + this(previous, combinator, group, column, comparator, null, value, false); } private Criteria(@Nullable Criteria previous, Combinator combinator, List group, - @Nullable SqlIdentifier column, @Nullable Comparator comparator, @Nullable Object value, boolean ignoreCase) { + @Nullable SqlIdentifier column, @Nullable Comparator comparator, @Nullable ExtendedComparator extendedComparator, @Nullable Object value, boolean ignoreCase) { this.previous = previous; this.combinator = previous != null && previous.isEmpty() ? Combinator.INITIAL : combinator; this.group = group; this.column = column; this.comparator = comparator; + this.extendedComparator = extendedComparator; this.value = value; this.ignoreCase = ignoreCase; } @@ -97,6 +103,7 @@ private Criteria(@Nullable Criteria previous, Combinator combinator, List criteria) { */ public Criteria ignoreCase(boolean ignoreCase) { if (this.ignoreCase != ignoreCase) { - return new Criteria(previous, combinator, group, column, comparator, value, ignoreCase); + return new Criteria(previous, combinator, group, column, comparator, extendedComparator, value, ignoreCase); } return this; } @@ -363,6 +370,11 @@ public Comparator getComparator() { return comparator; } + @Override + public ExtendedComparator getExtendedComparator() { + return extendedComparator; + } + /** * @return the comparison value. Can be {@literal null}. */ @@ -408,12 +420,13 @@ public boolean equals(Object o) { && Objects.equals(group, criteria.group) // && Objects.equals(column, criteria.column) // && comparator == criteria.comparator // + && extendedComparator == criteria.extendedComparator // && Objects.equals(value, criteria.value); } @Override public int hashCode() { - return Objects.hash(previous, combinator, group, column, comparator, value, ignoreCase); + return Objects.hash(previous, combinator, group, column, comparator, extendedComparator, value, ignoreCase); } private void unroll(CriteriaDefinition criteria, StringBuilder stringBuilder) { @@ -479,29 +492,35 @@ private void render(CriteriaDefinition criteria, StringBuilder stringBuilder) { return; } - stringBuilder.append(criteria.getColumn().toSql(IdentifierProcessing.NONE)).append(' ') - .append(criteria.getComparator().getComparator()); + stringBuilder.append(criteria.getColumn().toSql(IdentifierProcessing.NONE)).append(' '); + + if (criteria.getExtendedComparator() != null) { + stringBuilder.append(criteria.getExtendedComparator().operator()).append(' ').append(renderValue(criteria.getValue())); + } else { - switch (criteria.getComparator()) { - case BETWEEN: - case NOT_BETWEEN: - Pair pair = (Pair) criteria.getValue(); - stringBuilder.append(' ').append(pair.getFirst()).append(" AND ").append(pair.getSecond()); - break; + stringBuilder.append(criteria.getComparator().getComparator()); - case IS_NULL: - case IS_NOT_NULL: - case IS_TRUE: - case IS_FALSE: - break; + 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 IN: - case NOT_IN: - stringBuilder.append(" (").append(renderValue(criteria.getValue())).append(')'); - break; + case IS_NULL: + case IS_NOT_NULL: + case IS_TRUE: + case IS_FALSE: + break; - default: - stringBuilder.append(' ').append(renderValue(criteria.getValue())); + case IN: + case NOT_IN: + stringBuilder.append(" (").append(renderValue(criteria.getValue())).append(')'); + break; + + default: + stringBuilder.append(' ').append(renderValue(criteria.getValue())); + } } } 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 ac69107c31..941469bb3d 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 @@ -98,6 +98,12 @@ static CriteriaDefinition from(List criteria) { @Nullable Comparator getComparator(); + /** + * @return {@link ExtendedComparator}. + */ + @Nullable + ExtendedComparator getExtendedComparator(); + /** * @return the comparison value. Can be {@literal null}. */ @@ -141,7 +147,7 @@ enum Combinator { enum Comparator { 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"), ARRAY_CONTAINS("@>"); + "LIKE"), NOT_LIKE("NOT LIKE"), NOT_IN("NOT IN"), IN("IN"), IS_TRUE("IS TRUE"), IS_FALSE("IS FALSE"); private final String comparator; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/ExtendedComparator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/ExtendedComparator.java new file mode 100644 index 0000000000..32039d0494 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/ExtendedComparator.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020-2025 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; + +/** + * Analog of {@link org.springframework.data.relational.core.query.CriteriaDefinition.Comparator} for the extended operators, + * that are not commonly supported by RDBMS vendors. + * + * @author Mikhail Polivakha + */ +interface ExtendedComparator { + + String operator(); + + /** + * PostgreSQL specific operator for checking if the SQL ARRAY contains the given sub-array. + * + * @author Mikhail Polivakha + */ + enum PostgresExtendedContains implements ExtendedComparator { + + INSTANCE; + + @Override + public String operator() { + return "@>"; + } + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Postgres.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Postgres.java index b5b8251b79..82d56abb0c 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Postgres.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Postgres.java @@ -16,15 +16,13 @@ package org.springframework.data.relational.core.query; -import static org.springframework.data.relational.core.query.Criteria.*; +import static org.springframework.data.relational.core.query.Criteria.CriteriaLiteral; import java.sql.JDBCType; import java.util.StringJoiner; import org.jetbrains.annotations.NotNull; -import org.springframework.core.ResolvableType; import org.springframework.data.relational.core.sql.SqlIdentifier; -import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; /** @@ -64,10 +62,11 @@ public PostgresCriteriaArray(String arrayColumnName) { @NotNull @Override - public Criteria contains(Object... values) { + public Criteria + contains(Object... values) { Assert.notNull(values, "values array cannot be null"); - return new Criteria(SqlIdentifier.quoted(arrayColumnName), CriteriaDefinition.Comparator.ARRAY_CONTAINS, new CriteriaLiteral() { + return new Criteria(SqlIdentifier.quoted(arrayColumnName), ExtendedComparator.PostgresExtendedContains.INSTANCE, new CriteriaLiteral() { @Override public String getLiteral() { 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 74b16feace..57888cfc07 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 @@ -230,7 +230,7 @@ void shouldBuildSimplePredefinedDialectCriteriaCondition() { assertSoftly(softAssertions -> { softAssertions.assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.quoted("foo")); - softAssertions.assertThat(criteria.getComparator()).isEqualTo(Comparator.ARRAY_CONTAINS); + softAssertions.assertThat(criteria.getComparator()).isNull(); softAssertions.assertThat(criteria.getValue()).isInstanceOf(CriteriaLiteral.class); softAssertions .assertThat(criteria.getValue())