Skip to content

Implemented Postgres-specific array Criteria operations #1981

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
* @author Oliver Drotbohm
* @author Roman Chigvintsev
* @author Jens Schauder
* @author Mikhail Polivakha
* @since 2.0
*/
public class Criteria implements CriteriaDefinition {
Expand All @@ -65,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;

private Criteria(SqlIdentifier column, 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<CriteriaDefinition> 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<CriteriaDefinition> 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;
}
Expand All @@ -96,6 +103,7 @@ private Criteria(@Nullable Criteria previous, Combinator combinator, List<Criter
this.group = group;
this.column = null;
this.comparator = null;
this.extendedComparator = null;
this.value = null;
this.ignoreCase = false;
}
Expand Down Expand Up @@ -259,7 +267,7 @@ public Criteria or(List<? extends CriteriaDefinition> 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;
}
Expand Down Expand Up @@ -328,13 +336,15 @@ private boolean doIsEmpty() {
/**
* @return {@literal true} if this {@link Criteria} is empty.
*/
@Override
public boolean isGroup() {
return !this.group.isEmpty();
}

/**
* @return {@link Combinator} to combine this criteria with a previous one.
*/
@Override
public Combinator getCombinator() {
return combinator;
}
Expand All @@ -360,6 +370,11 @@ public Comparator getComparator() {
return comparator;
}

@Override
public ExtendedComparator getExtendedComparator() {
return extendedComparator;
}

/**
* @return the comparison value. Can be {@literal null}.
*/
Expand Down Expand Up @@ -405,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) {
Expand Down Expand Up @@ -476,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<Object, Object> pair = (Pair<Object, Object>) 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<Object, Object> pair = (Pair<Object, Object>) 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()));
}
}
}

Expand All @@ -515,6 +537,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);
}
Expand Down Expand Up @@ -653,6 +679,12 @@ public interface CriteriaStep {
* @return a new {@link Criteria} object
*/
Criteria isFalse();

}

static interface CriteriaLiteral {

String getLiteral();
}

/**
Expand Down Expand Up @@ -812,7 +844,7 @@ public Criteria isFalse() {
return createCriteria(Comparator.IS_FALSE, false);
}

protected Criteria createCriteria(Comparator comparator, @Nullable Object value) {
protected Criteria createCriteria(@Nullable Comparator comparator, @Nullable Object value) {
return new Criteria(this.property, comparator, value);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
*
* @author Mark Paluch
* @author Jens Schauder
* @author Mikhail Polivakha
* @since 2.0
*/
public interface CriteriaDefinition {
Expand Down Expand Up @@ -97,6 +98,12 @@ static CriteriaDefinition from(List<? extends CriteriaDefinition> criteria) {
@Nullable
Comparator getComparator();

/**
* @return {@link ExtendedComparator}.
*/
@Nullable
ExtendedComparator getExtendedComparator();

/**
* @return the comparison value. Can be {@literal null}.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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 "@>";
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Loading
Loading