diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/binding/AnonymousBindMarkers.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/binding/AnonymousBindMarkers.java
new file mode 100644
index 0000000000..3d2a256c6c
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/binding/AnonymousBindMarkers.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 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.binding;
+
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+
+import org.springframework.data.relational.core.sql.BindMarker;
+
+/**
+ * Anonymous, index-based bind markers that use a static placeholder.
+ *
+ * Instances are bound by the ordinal position ordered by the appearance of the placeholder. This implementation creates
+ * indexed bind markers using an anonymous placeholder that correlates with an index.
+ *
+ * Note: Anonymous bind markers are problematic because they have to appear in generated SQL in the same order they get
+ * generated. This might cause challenges in the future with complex generate statements. For example those containing
+ * subselects which limit the freedom of arranging bind markers.
+ *
+ * @author Mark Paluch
+ */
+class AnonymousBindMarkers implements BindMarkers {
+
+ private static final AtomicIntegerFieldUpdater COUNTER_INCREMENTER = AtomicIntegerFieldUpdater
+ .newUpdater(AnonymousBindMarkers.class, "counter");
+
+ private final String placeholder;
+
+ // access via COUNTER_INCREMENTER
+ @SuppressWarnings("unused") private volatile int counter;
+
+ /**
+ * Create a new {@link AnonymousBindMarkers} instance for the given {@code placeholder}.
+ *
+ * @param placeholder parameter bind marker
+ */
+ AnonymousBindMarkers(String placeholder) {
+ this.placeholder = placeholder;
+ }
+
+ @Override
+ public BindMarker next() {
+ int index = COUNTER_INCREMENTER.getAndIncrement(this);
+ return BindMarker.indexed(this.placeholder, index);
+ }
+
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/binding/BindMarkers.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/binding/BindMarkers.java
new file mode 100644
index 0000000000..27045fc453
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/binding/BindMarkers.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 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.binding;
+
+import org.springframework.data.relational.core.sql.BindMarker;
+
+/**
+ * Bind markers represent placeholders in SQL queries for substitution for an actual parameter. Using bind markers
+ * allows creating safe queries so query strings are not required to contain escaped values but rather the driver
+ * encodes the parameter in the appropriate representation.
+ *
+ * {@link BindMarkers} is stateful and can be only used for a single binding pass of one or more parameters. It
+ * maintains bind indexes or bind parameter names.
+ *
+ * @author Mark Paluch
+ * @since 5.3
+ * @see BindMarker
+ * @see BindMarkersFactory
+ */
+@FunctionalInterface
+public interface BindMarkers {
+
+ /**
+ * Create a new {@link BindMarker}.
+ */
+ BindMarker next();
+
+ /**
+ * Create a new {@link BindMarker} that accepts a {@code hint}.
+ *
+ * Implementations are allowed to consider/ignore/filter the name hint to create more expressive bind markers.
+ *
+ * @param hint an optional name hint that can be used as part of the bind marker
+ * @return a new {@link BindMarker}
+ */
+ default BindMarker next(String hint) {
+ return next();
+ }
+
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/binding/BindMarkersFactory.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/binding/BindMarkersFactory.java
new file mode 100644
index 0000000000..333b9dc5a9
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/binding/BindMarkersFactory.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 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.binding;
+
+import java.util.function.Function;
+
+import org.springframework.data.relational.core.sql.BindMarker;
+import org.springframework.util.Assert;
+
+/**
+ * This class creates new {@link BindMarkers} instances to bind parameter to a specific statement.
+ *
+ * Bind markers can be typically represented as placeholder and identifier. Placeholders are used within the query to
+ * execute so the underlying database system can substitute the placeholder with the actual value. Identifiers are used
+ * in R2DBC drivers to bind a value to a bind marker. Identifiers are typically a part of an entire bind marker when
+ * using indexed or named bind markers.
+ *
+ * @author Mark Paluch
+ * @see BindMarkers
+ */
+@FunctionalInterface
+public interface BindMarkersFactory {
+
+ /**
+ * Create a new {@link BindMarkers} instance.
+ */
+ BindMarkers create();
+
+ /**
+ * Return whether the {@link BindMarkersFactory} uses identifiable placeholders: {@code false} if multiple
+ * placeholders cannot be distinguished by just the {@link BindMarker#getPlaceholder() placeholder} identifier.
+ */
+ default boolean identifiablePlaceholders() {
+ return true;
+ }
+
+ // Static factory methods
+
+ /**
+ * Create index-based {@link BindMarkers} using indexes to bind parameters. Allows customization of the bind marker
+ * placeholder {@code prefix} to represent the bind marker as placeholder within the query.
+ *
+ * @param prefix bind parameter prefix that is included in {@link BindMarker#getPlaceholder()} but not the actual
+ * identifier
+ * @param beginWith the first index to use
+ * @return a {@link BindMarkersFactory} using {@code prefix} and {@code beginWith}
+ */
+ static BindMarkersFactory indexed(String prefix, int beginWith) {
+ Assert.notNull(prefix, "Prefix must not be null");
+ return () -> new IndexedBindMarkers(prefix, beginWith);
+ }
+
+ /**
+ * Create anonymous, index-based bind marker using a static placeholder. Instances are bound by the ordinal position
+ * ordered by the appearance of the placeholder. This implementation creates indexed bind markers using an anonymous
+ * placeholder that correlates with an index.
+ *
+ * @param placeholder parameter placeholder
+ * @return a {@link BindMarkersFactory} using {@code placeholder}
+ */
+ static BindMarkersFactory anonymous(String placeholder) {
+ Assert.hasText(placeholder, "Placeholder must not be empty");
+ return new BindMarkersFactory() {
+ @Override
+ public BindMarkers create() {
+ return new AnonymousBindMarkers(placeholder);
+ }
+
+ @Override
+ public boolean identifiablePlaceholders() {
+ return false;
+ }
+ };
+ }
+
+ /**
+ * Create named {@link BindMarkers} using identifiers to bind parameters. Named bind markers can support
+ * {@link BindMarkers#next(String) name hints}. If no {@link BindMarkers#next(String) hint} is given, named bind
+ * markers can use a counter or a random value source to generate unique bind markers. Allows customization of the
+ * bind marker placeholder {@code prefix} and {@code namePrefix} to represent the bind marker as placeholder within
+ * the query.
+ */
+ static BindMarkersFactory named(String prefix, String namePrefix, int maxLength) {
+ return named(prefix, namePrefix, maxLength, Function.identity());
+ }
+
+ /**
+ * Create named {@link BindMarkers} using identifiers to bind parameters. Named bind markers support
+ * {@link BindMarkers#next(String) name hints}. If no {@link BindMarkers#next(String) hint} is given, named bind
+ * markers can use a counter or a random value source to generate unique bind markers.
+ *
+ * @param hintFilterFunction filter {@link Function} to consider database-specific limitations in bind marker/variable
+ * names such as ASCII chars only
+ * @return a {@link BindMarkersFactory} using {@code prefix} and {@code beginWith}
+ */
+ static BindMarkersFactory named(String prefix, String namePrefix, int maxLength,
+ Function hintFilterFunction) {
+
+ Assert.notNull(prefix, "Prefix must not be null");
+ Assert.notNull(namePrefix, "Index prefix must not be null");
+ Assert.notNull(hintFilterFunction, "Hint filter function must not be null");
+ return () -> new NamedBindMarkers(prefix, namePrefix, maxLength, hintFilterFunction);
+ }
+
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/binding/IndexedBindMarkers.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/binding/IndexedBindMarkers.java
new file mode 100644
index 0000000000..a54b544882
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/binding/IndexedBindMarkers.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 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.binding;
+
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+
+import org.springframework.data.relational.core.sql.BindMarker;
+
+/**
+ * Index-based bind markers. This implementation creates indexed bind markers using a numeric index and an optional
+ * prefix for bind markers to be represented within the query string.
+ *
+ * @author Mark Paluch
+ * @author Jens Schauder
+ */
+class IndexedBindMarkers implements BindMarkers {
+
+ private static final AtomicIntegerFieldUpdater COUNTER_INCREMENTER = AtomicIntegerFieldUpdater
+ .newUpdater(IndexedBindMarkers.class, "counter");
+
+ private final int offset;
+
+ private final String prefix;
+
+ // access via COUNTER_INCREMENTER
+ @SuppressWarnings("unused") private volatile int counter;
+
+ /**
+ * Create a new indexed instance for the given {@code prefix} and {@code beginWith} value.
+ *
+ * @param prefix the bind parameter prefix
+ * @param beginIndex the first index to use
+ */
+ IndexedBindMarkers(String prefix, int beginIndex) {
+ this.counter = 0;
+ this.prefix = prefix;
+ this.offset = beginIndex;
+ }
+
+ @Override
+ public BindMarker next() {
+ int index = COUNTER_INCREMENTER.getAndIncrement(this);
+ return BindMarker.indexed(this.prefix + "" + (index + this.offset), index);
+ }
+
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/binding/NamedBindMarkers.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/binding/NamedBindMarkers.java
new file mode 100644
index 0000000000..c2f93e9d2d
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/binding/NamedBindMarkers.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 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.binding;
+
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+import java.util.function.Function;
+
+import org.springframework.data.relational.core.sql.BindMarker;
+import org.springframework.util.Assert;
+
+/**
+ * Name-based bind markers.
+ *
+ * @author Mark Paluch
+ */
+class NamedBindMarkers implements BindMarkers {
+
+ private static final AtomicIntegerFieldUpdater COUNTER_INCREMENTER = AtomicIntegerFieldUpdater
+ .newUpdater(NamedBindMarkers.class, "counter");
+
+ private final String prefix;
+
+ private final String namePrefix;
+
+ private final int nameLimit;
+
+ private final Function hintFilterFunction;
+
+ // access via COUNTER_INCREMENTER
+ @SuppressWarnings("unused") private volatile int counter;
+
+ NamedBindMarkers(String prefix, String namePrefix, int nameLimit, Function hintFilterFunction) {
+ this.prefix = prefix;
+ this.namePrefix = namePrefix;
+ this.nameLimit = nameLimit;
+ this.hintFilterFunction = hintFilterFunction;
+ }
+
+ @Override
+ public BindMarker next() {
+ String name = nextName();
+ return BindMarker.named(this.prefix + name);
+ }
+
+ @Override
+ public BindMarker next(String hint) {
+ Assert.notNull(hint, "Name hint must not be null");
+ String name = nextName() + this.hintFilterFunction.apply(hint);
+
+ if (name.length() > this.nameLimit) {
+ name = name.substring(0, this.nameLimit);
+ }
+
+ return BindMarker.named(this.prefix + name);
+ }
+
+ private String nextName() {
+ int index = COUNTER_INCREMENTER.getAndIncrement(this);
+ return this.namePrefix + index;
+ }
+
+}
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 a06b4e3b25..dbe7203360 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
@@ -70,7 +70,7 @@ public String createSequenceQuery(SqlIdentifier sequenceName) {
}
};
- protected PostgresDialect() {}
+ public PostgresDialect() {}
private static final LimitClause LIMIT_CLAUSE = new LimitClause() {
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Bindings.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Bindings.java
new file mode 100644
index 0000000000..81d0759fd1
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Bindings.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 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 org.springframework.data.relational.core.sql.BindMarker;
+
+/**
+ * @author Mark Paluch
+ */
+public interface Bindings {
+
+ void bind(BindMarker bindMarker, Object value);
+
+ void bind(BindMarker bindMarker, Object value, QueryExpression.ExpressionTypeContext typeContext);
+
+}
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..135dbe908d 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
@@ -15,6 +15,8 @@
*/
package org.springframework.data.relational.core.query;
+import static org.springframework.data.relational.core.query.CriteriaDefinition.Comparator.*;
+
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -63,26 +65,39 @@ public class Criteria implements CriteriaDefinition {
private final Combinator combinator;
private final List group;
+ private final @Nullable QueryExpression queryExpression;
private final @Nullable SqlIdentifier column;
private final @Nullable Comparator comparator;
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);
+ protected Criteria(QueryExpression expression) {
+ this(null, Combinator.INITIAL, Collections.emptyList(), expression, null, null, null, false);
+ }
+
+ protected Criteria(SqlIdentifier column, Comparator comparator, @Nullable Object value) {
+ this(null, Combinator.INITIAL, Collections.emptyList(), null, column, comparator, 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);
+ protected Criteria(QueryExpression queryExpression, @Nullable SqlIdentifier column, Comparator comparator,
+ @Nullable Object value) {
+ this(null, Combinator.INITIAL, Collections.emptyList(), queryExpression, column, comparator, value, false);
}
- private Criteria(@Nullable Criteria previous, Combinator combinator, List group,
- @Nullable SqlIdentifier column, @Nullable Comparator comparator, @Nullable Object value, boolean ignoreCase) {
+ protected Criteria(@Nullable Criteria previous, Combinator combinator, List group,
+ @Nullable QueryExpression queryExpression, @Nullable SqlIdentifier column, @Nullable Comparator comparator,
+ @Nullable Object value) {
+ this(previous, combinator, group, queryExpression, column, comparator, value, false);
+ }
+
+ protected Criteria(@Nullable Criteria previous, Combinator combinator, List group,
+ @Nullable QueryExpression queryExpression, @Nullable SqlIdentifier column, @Nullable Comparator comparator,
+ @Nullable Object value, boolean ignoreCase) {
this.previous = previous;
this.combinator = previous != null && previous.isEmpty() ? Combinator.INITIAL : combinator;
this.group = group;
+ this.queryExpression = queryExpression;
this.column = column;
this.comparator = comparator;
this.value = value;
@@ -92,6 +107,7 @@ private Criteria(@Nullable Criteria previous, Combinator combinator, List group) {
this.previous = previous;
+ this.queryExpression = null;
this.combinator = previous != null && previous.isEmpty() ? Combinator.INITIAL : combinator;
this.group = group;
this.column = null;
@@ -153,7 +169,7 @@ public static CriteriaStep where(String column) {
Assert.hasText(column, "Column name must not be null or empty");
- return new DefaultCriteriaStep(SqlIdentifier.unquoted(column));
+ return new DefaultCriteriaStep(QueryExpression.column(column), SqlIdentifier.unquoted(column));
}
/**
@@ -167,10 +183,30 @@ public CriteriaStep and(String column) {
Assert.hasText(column, "Column name must not be null or empty");
SqlIdentifier identifier = SqlIdentifier.unquoted(column);
- return new DefaultCriteriaStep(identifier) {
+ QueryExpression lhs = QueryExpression.column(column);
+ return new DefaultCriteriaStep(lhs, identifier) {
+ @Override
+ protected Criteria createCriteria(Comparator comparator, @Nullable Object value) {
+ return new Criteria(Criteria.this, Combinator.AND, Collections.emptyList(), lhs, identifier, comparator, value);
+ }
+ };
+ }
+
+ /**
+ * Create a new {@link Criteria} and combine it with {@code AND} using the provided {@code expression}.
+ *
+ * @param expression must not be {@literal null} or empty.
+ * @return a new {@link CriteriaStep} object to complete the next {@link Criteria}.
+ */
+ public CriteriaStep and(QueryExpression expression) {
+
+ Assert.notNull(expression, "Query expression must not be null");
+
+ return new DefaultCriteriaStep(expression) {
@Override
protected Criteria createCriteria(Comparator comparator, @Nullable Object value) {
- return new Criteria(Criteria.this, Combinator.AND, Collections.emptyList(), identifier, comparator, value);
+ return new Criteria(Criteria.this, Combinator.AND, Collections.emptyList(), expression, null, comparator,
+ value);
}
};
}
@@ -214,10 +250,29 @@ public CriteriaStep or(String column) {
Assert.hasText(column, "Column name must not be null or empty");
SqlIdentifier identifier = SqlIdentifier.unquoted(column);
- return new DefaultCriteriaStep(identifier) {
+ QueryExpression lhs = QueryExpression.column(column);
+ return new DefaultCriteriaStep(lhs, identifier) {
@Override
protected Criteria createCriteria(Comparator comparator, @Nullable Object value) {
- return new Criteria(Criteria.this, Combinator.OR, Collections.emptyList(), identifier, comparator, value);
+ return new Criteria(Criteria.this, Combinator.OR, Collections.emptyList(), lhs, identifier, comparator, value);
+ }
+ };
+ }
+
+ /**
+ * Create a new {@link Criteria} and combine it with {@code OR} using the provided {@code expression}.
+ *
+ * @param expression must not be {@literal null} or empty.
+ * @return a new {@link CriteriaStep} object to complete the next {@link Criteria}.
+ */
+ public CriteriaStep or(QueryExpression expression) {
+
+ Assert.notNull(expression, "Query expression must not be null");
+
+ return new DefaultCriteriaStep(expression) {
+ @Override
+ protected Criteria createCriteria(Comparator comparator, @Nullable Object value) {
+ return new Criteria(Criteria.this, Combinator.OR, Collections.emptyList(), expression, null, comparator, value);
}
};
}
@@ -259,7 +314,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, queryExpression, column, comparator, value, ignoreCase);
}
return this;
}
@@ -315,6 +370,10 @@ private boolean doIsEmpty() {
return false;
}
+ if (this.queryExpression != null) {
+ return false;
+ }
+
for (CriteriaDefinition criteria : group) {
if (!criteria.isEmpty()) {
@@ -344,6 +403,12 @@ public List getGroup() {
return group;
}
+ @Nullable
+ @Override
+ public QueryExpression getExpression() {
+ return queryExpression;
+ }
+
/**
* @return the column/property name.
*/
@@ -476,7 +541,13 @@ private void render(CriteriaDefinition criteria, StringBuilder stringBuilder) {
return;
}
- stringBuilder.append(criteria.getColumn().toSql(IdentifierProcessing.NONE)).append(' ')
+ stringBuilder.append(getLhs(criteria));
+
+ if (criteria.getComparator() == null) {
+ return;
+ }
+
+ stringBuilder.append(' ')
.append(criteria.getComparator().getComparator());
switch (criteria.getComparator()) {
@@ -502,6 +573,19 @@ private void render(CriteriaDefinition criteria, StringBuilder stringBuilder) {
}
}
+ private static String getLhs(CriteriaDefinition criteria) {
+
+ if (criteria.hasExpression()) {
+ return criteria.getExpression().toString();
+ }
+
+ if (criteria.hasColumn()) {
+ return criteria.getColumn().toSql(IdentifierProcessing.NONE);
+ }
+
+ return "?";
+ }
+
private static String renderValue(@Nullable Object value) {
if (value instanceof Number) {
@@ -658,11 +742,18 @@ public interface CriteriaStep {
/**
* Default {@link CriteriaStep} implementation.
*/
- static class DefaultCriteriaStep implements CriteriaStep {
+ protected static class DefaultCriteriaStep implements CriteriaStep {
+
+ private final QueryExpression lhs;
+ private final @Nullable SqlIdentifier property;
- private final SqlIdentifier property;
+ protected DefaultCriteriaStep(QueryExpression lhs) {
+ this.lhs = lhs;
+ this.property = null;
+ }
- DefaultCriteriaStep(SqlIdentifier property) {
+ DefaultCriteriaStep(QueryExpression lhs, SqlIdentifier property) {
+ this.lhs = lhs;
this.property = property;
}
@@ -734,7 +825,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(BETWEEN, Pair.of(begin, end));
}
@Override
@@ -812,8 +903,12 @@ public Criteria isFalse() {
return createCriteria(Comparator.IS_FALSE, false);
}
+ protected QueryExpression getLhs() {
+ return lhs;
+ }
+
protected Criteria createCriteria(Comparator comparator, @Nullable Object value) {
- return new Criteria(this.property, comparator, value);
+ return new Criteria(this.lhs, 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 c09129a1b6..2603719714 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
@@ -85,12 +85,35 @@ static CriteriaDefinition from(List extends CriteriaDefinition> criteria) {
List getGroup();
+ /**
+ * @return
+ * @since 4.0
+ */
+ @Nullable
+ QueryExpression getExpression();
+
+ /**
+ * @return returns {@literal true} if this criteria definition has a {@link QueryExpression}.
+ * @since 4.0
+ */
+ default boolean hasExpression() {
+ return getExpression() != null;
+ }
+
/**
* @return the column/property name.
*/
@Nullable
SqlIdentifier getColumn();
+ /**
+ * @return returns {@literal true} if this criteria definition has a column/property name.
+ * @since 4.0
+ */
+ default boolean hasColumn() {
+ return getColumn() != null;
+ }
+
/**
* @return {@link Criteria.Comparator}.
*/
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/CriteriaSources.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/CriteriaSources.java
new file mode 100644
index 0000000000..4c0328a073
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/CriteriaSources.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 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 org.springframework.data.relational.core.sql.Expression;
+import org.springframework.data.relational.core.sql.IdentifierProcessing;
+import org.springframework.data.relational.core.sql.SqlIdentifier;
+
+/**
+ * Utility class providing implementations of {@link CriteriaSource} for common sources such as columns and SQL
+ * identifiers.
+ *
+ * @author Mark Paluch
+ * @since 4.0
+ */
+class CriteriaSources {
+
+ /**
+ * A column name (or property path) reference.
+ *
+ * @param name
+ */
+ public record Column(String name) implements QueryExpression {
+
+ @Override
+ public ExpressionTypeContext getType(EvaluationContext context) {
+ return context.getColumn(name);
+ }
+
+ @Override
+ public String getNameHint() {
+ return name;
+ }
+
+ @Override
+ public Expression evaluate(EvaluationContext context) {
+ return context.getColumn(name).toExpression();
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+
+ /**
+ * A column or alias name.
+ *
+ * @param identifier
+ */
+ public record SqlIdentifierSource(SqlIdentifier identifier) implements QueryExpression {
+
+ @Override
+ public ExpressionTypeContext getType(EvaluationContext context) {
+ return context.getColumn(identifier);
+ }
+
+ @Override
+ public String getNameHint() {
+ return identifier.getReference();
+ }
+
+ @Override
+ public Expression evaluate(EvaluationContext context) {
+ return context.getColumn(identifier).toExpression();
+ }
+
+ @Override
+ public String toString() {
+ return identifier.toSql(IdentifierProcessing.NONE);
+ }
+
+ }
+
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/EvaluationContextSupport.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/EvaluationContextSupport.java
new file mode 100644
index 0000000000..d27501f7a1
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/EvaluationContextSupport.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright 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 java.util.regex.Pattern;
+
+import org.springframework.data.mapping.MappingException;
+import org.springframework.data.mapping.PersistentPropertyPath;
+import org.springframework.data.mapping.PropertyPath;
+import org.springframework.data.mapping.PropertyReferenceException;
+import org.springframework.data.mapping.context.MappingContext;
+import org.springframework.data.relational.core.conversion.RelationalConverter;
+import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
+import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
+import org.springframework.data.relational.core.sql.Column;
+import org.springframework.data.relational.core.sql.Expression;
+import org.springframework.data.relational.core.sql.SqlIdentifier;
+import org.springframework.data.relational.core.sql.Table;
+import org.springframework.data.relational.core.sql.TableLike;
+import org.springframework.data.util.TypeInformation;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+
+/**
+ * Support class for {@link QueryExpression.EvaluationContext} implementations.
+ *
+ * @author Mark Paluch
+ * @since 4.0
+ */
+public abstract class EvaluationContextSupport implements QueryExpression.EvaluationContext {
+
+ private final Table table;
+ private final RelationalConverter converter;
+ private final @Nullable RelationalPersistentEntity> entity;
+
+ public EvaluationContextSupport(Table table, RelationalConverter converter,
+ @Nullable RelationalPersistentEntity> entity) {
+
+ this.table = table;
+ this.converter = converter;
+ this.entity = entity;
+ }
+
+ protected EvaluationContextSupport(EvaluationContextSupport previous) {
+
+ this.table = previous.table;
+ this.converter = previous.converter;
+ this.entity = previous.entity;
+ }
+
+ protected RelationalConverter getConverter() {
+ return converter;
+ }
+
+ private Field createPropertyField(SqlIdentifier key) {
+ return entity != null ? new MetadataBackedField(key, entity, converter.getMappingContext()) : new Field(key);
+ }
+
+ @Override
+ public QueryExpression.MappedColumn getColumn(SqlIdentifier identifier) {
+
+ Field propertyField = createPropertyField(identifier);
+ TableLike table = getTable(identifier);
+ return new DefaultMappedColumn(table, table.column(propertyField.getMappedColumnName()), propertyField);
+ }
+
+ // TODO: Some tables can come from a JOIN, see JDBC
+ @Override
+ public QueryExpression.MappedColumn getColumn(String column) {
+
+ Field propertyField = createPropertyField(SqlIdentifier.unquoted(column));
+ TableLike table = getTable(column);
+ return new DefaultMappedColumn(table, table.column(propertyField.getMappedColumnName()), propertyField);
+ }
+
+ protected TableLike getTable(SqlIdentifier identifier) {
+ return table;
+ }
+
+ protected TableLike getTable(String column) {
+ return table;
+ }
+
+ @Override
+ public abstract QueryExpression.EvaluationContext withType(QueryExpression.ExpressionTypeContext type);
+
+ static class DefaultMappedColumn implements QueryExpression.MappedColumn {
+
+ private final TableLike table;
+ private final Column column;
+ private final Field field;
+
+ DefaultMappedColumn(TableLike table, Column column, Field field) {
+ this.table = table;
+ this.column = column;
+ this.field = field;
+ }
+
+ @Override
+ public Expression toExpression() {
+ return column;
+ }
+
+ @Override
+ public TypeInformation> getTargetType() {
+ return field.getTypeHint();
+ }
+
+ @Nullable
+ @Override
+ public RelationalPersistentProperty getProperty() {
+ return field.getProperty();
+ }
+
+ }
+
+ /**
+ * Value object to represent a field and its meta-information.
+ */
+ protected static class Field {
+
+ protected final SqlIdentifier name;
+
+ /**
+ * Creates a new {@link Field} without meta-information but the given name.
+ *
+ * @param name must not be {@literal null} or empty.
+ */
+ public Field(SqlIdentifier name) {
+
+ Assert.notNull(name, "Name must not be null");
+ this.name = name;
+ }
+
+ /**
+ * Returns the key to be used in the mapped document eventually.
+ *
+ * @return
+ */
+ public SqlIdentifier getMappedColumnName() {
+ return this.name;
+ }
+
+ public TypeInformation> getTypeHint() {
+ return TypeInformation.OBJECT;
+ }
+
+ public @Nullable RelationalPersistentProperty getProperty() {
+ return null;
+ }
+ }
+
+ /**
+ * Extension of {@link Field} to be backed with mapping metadata.
+ */
+ protected static class MetadataBackedField extends Field {
+
+ private final RelationalPersistentEntity> entity;
+ private final MappingContext extends RelationalPersistentEntity>, ? extends RelationalPersistentProperty> mappingContext;
+ private final @Nullable RelationalPersistentProperty property;
+ private final @Nullable PersistentPropertyPath extends RelationalPersistentProperty> path;
+
+ /**
+ * Creates a new {@link MetadataBackedField} with the given name, {@link RelationalPersistentEntity} and
+ * {@link MappingContext}.
+ *
+ * @param name must not be {@literal null} or empty.
+ * @param entity must not be {@literal null}.
+ * @param context must not be {@literal null}.
+ */
+ protected MetadataBackedField(SqlIdentifier name, RelationalPersistentEntity> entity,
+ MappingContext extends RelationalPersistentEntity>, ? extends RelationalPersistentProperty> context) {
+ this(name, entity, context, null);
+ }
+
+ /**
+ * Creates a new {@link MetadataBackedField} with the given name, {@link RelationalPersistentEntity} and
+ * {@link MappingContext} with the given {@link RelationalPersistentProperty}.
+ *
+ * @param name must not be {@literal null} or empty.
+ * @param entity must not be {@literal null}.
+ * @param context must not be {@literal null}.
+ * @param property may be {@literal null}.
+ */
+ protected MetadataBackedField(SqlIdentifier name, RelationalPersistentEntity> entity,
+ MappingContext extends RelationalPersistentEntity>, ? extends RelationalPersistentProperty> context,
+ @Nullable RelationalPersistentProperty property) {
+
+ super(name);
+
+ Assert.notNull(entity, "RelationalPersistentEntity must not be null");
+
+ this.entity = entity;
+ this.mappingContext = context;
+
+ this.path = getPath(name.getReference());
+ this.property = this.path == null ? property : this.path.getLeafProperty();
+ }
+
+ @Override
+ public SqlIdentifier getMappedColumnName() {
+ return this.path == null || this.path.getLeafProperty() == null ? super.getMappedColumnName()
+ : this.path.getLeafProperty().getColumnName();
+ }
+
+ /**
+ * Returns the {@link PersistentPropertyPath} for the given {@code pathExpression}.
+ *
+ * @param pathExpression the path expression to use.
+ * @return
+ */
+ @Nullable
+ private PersistentPropertyPath extends RelationalPersistentProperty> getPath(String pathExpression) {
+
+ try {
+
+ PropertyPath path = forName(pathExpression);
+
+ if (isPathToJavaLangClassProperty(path)) {
+ return null;
+ }
+
+ return this.mappingContext.getPersistentPropertyPath(path);
+ } catch (MappingException | PropertyReferenceException e) {
+ return null;
+ }
+ }
+
+ private PropertyPath forName(String path) {
+
+ if (entity.getPersistentProperty(path) != null) {
+ return PropertyPath.from(Pattern.quote(path), entity.getTypeInformation());
+ }
+
+ return PropertyPath.from(path, entity.getTypeInformation());
+ }
+
+ private boolean isPathToJavaLangClassProperty(PropertyPath path) {
+ return path.getType().equals(Class.class) && path.getLeafProperty().getOwningType().getType().equals(Class.class);
+ }
+
+ @Override
+ public TypeInformation> getTypeHint() {
+
+ if (this.property == null) {
+ return super.getTypeHint();
+ }
+
+ if (this.property.getType().isPrimitive()) {
+ return TypeInformation.of(ClassUtils.resolvePrimitiveIfNecessary(this.property.getType()));
+ }
+
+ if (this.property.getType().isArray()) {
+ return this.property.getTypeInformation();
+ }
+
+ if (this.property.getType().isInterface()
+ || (java.lang.reflect.Modifier.isAbstract(this.property.getType().getModifiers()))) {
+ return TypeInformation.OBJECT;
+ }
+
+ return this.property.getTypeInformation();
+ }
+
+ @Nullable
+ @Override
+ public RelationalPersistentProperty getProperty() {
+ return this.property;
+ }
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/NestedQueryExpression.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/NestedQueryExpression.java
new file mode 100644
index 0000000000..1d59aa87f6
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/NestedQueryExpression.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 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 org.springframework.data.relational.core.sql.Expression;
+import org.springframework.data.relational.core.sql.Expressions;
+import org.springframework.lang.Nullable;
+
+/**
+ * Query expression that nests another {@link QueryExpression}. This is used to ensure that the nested expression
+ *
+ * @author Mark Paluch
+ */
+class NestedQueryExpression implements QueryExpression {
+
+ private final QueryExpression expression;
+
+ public NestedQueryExpression(QueryExpression expression) {
+ this.expression = expression;
+ }
+
+ @Override
+ public QueryExpression nest() {
+ return this;
+ }
+
+ @Override
+ public ExpressionTypeContext getType(EvaluationContext context) {
+ return expression.getType(context);
+ }
+
+ @Nullable
+ @Override
+ public String getNameHint() {
+ return expression.getNameHint();
+ }
+
+ @Override
+ public Expression evaluate(EvaluationContext context) {
+ return Expressions.nest(expression.evaluate(context));
+ }
+
+ @Override
+ public String toString() {
+ return "(" + expression + ")";
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/PgSql.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/PgSql.java
new file mode 100644
index 0000000000..c16e83566f
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/PgSql.java
@@ -0,0 +1,1003 @@
+/*
+ * Copyright 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 java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import org.springframework.data.domain.ScoringFunction;
+import org.springframework.data.domain.Vector;
+import org.springframework.data.relational.core.query.PgSqlImpl.ValuesExpression;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * Dialect-specific extension for the Postgres database dialect.
+ *
+ * @author Mark Paluch
+ * @since 4.0
+ */
+public final class PgSql {
+
+ private PgSql() {
+
+ }
+
+ /**
+ * Creates a function expression for the given {@code functionName} using {@code arguments}. The function name is
+ * passed on directly to the SQL text so it must be a valid function name. Make sure to sanitize the function name
+ * when accepting it from user input.
+ *
+ * @param functionName name of the function to call, must not be {@literal null} or empty.
+ * @param arguments function arguments, can be any {@link QueryExpression} or simple values like {@code String},
+ * {@code Integer}.
+ * @return the function expression for {@code functionName} with the given {@code arguments}.
+ */
+ public static PostgresQueryExpression function(String functionName, Object... arguments) {
+ return new PgSqlImpl.FunctionExpression(functionName, Arrays.asList(arguments));
+ }
+
+ /**
+ * Entry point for creating a {@link PgCriteria.PostgresCriteriaStep} for the given column (or property specified as
+ * property path).
+ *
+ * @param column
+ * @return
+ */
+ public static PgCriteria.PostgresCriteriaStep where(String column) {
+ return PgCriteria.where(column);
+ }
+
+ /**
+ * Entry point for creating a {@link PgCriteria.PostgresCriteriaStep} for the given column (or property specified as
+ * property path) that shall be wrapped by a wrapping function to process the column contents before its use in a
+ * downstream condition or expression.
+ *
+ * @param column
+ * @param wrappingFunction
+ * @return
+ */
+ public static PgCriteria.PostgresCriteriaStep where(String column,
+ Function wrappingFunction) {
+ return PgCriteria.where(column, wrappingFunction);
+ }
+
+ /**
+ * Entry point for creating a {@link PgCriteria.PostgresCriteriaStep} for the given {@link QueryExpression
+ * expression}.
+ *
+ * @param expression
+ * @return
+ */
+ public static PgCriteria.PostgresCriteriaStep where(QueryExpression expression) {
+ return PgCriteria.where(expression);
+ }
+
+ /**
+ * Entry point for creating a {@link PgCriteria.PostgresCriteriaStep} for the given {@link QueryExpression} that shall
+ * be wrapped by a wrapping function to process the column contents before its use in a downstream condition or
+ * expression.
+ *
+ * @param expression
+ * @param wrappingFunction
+ * @return
+ */
+ public static PgCriteria.PostgresCriteriaStep where(QueryExpression expression,
+ Function wrappingFunction) {
+ return PgCriteria.where(expression, wrappingFunction);
+ }
+
+ /**
+ * Entrypoint for array functions.
+ *
+ * @return
+ */
+ public static ArrayFunctions arrays() {
+
+ return new ArrayFunctions() {
+ @Override
+ public QueryExpression arrayOf(Object... values) {
+ return new PgSqlImpl.ArrayExpression(Arrays.asList(values));
+ }
+ };
+ }
+
+ /**
+ * Entrypoint for JSON functions.
+ *
+ * @return
+ */
+ public static JsonFunctions json() {
+
+ return new JsonFunctions() {
+
+ @Override
+ public QueryExpression jsonOf(Map jsonObject) {
+ return new PgSqlImpl.JsonExpression(jsonObject, "json");
+ }
+
+ @Override
+ public QueryExpression jsonbOf(Map jsonObject) {
+ return new PgSqlImpl.JsonExpression(jsonObject, "jsonb");
+ }
+ };
+ }
+
+ public static VectorSearchFunctions vectorSearch() {
+ return new VectorSearchFunctions() {
+ @Override
+ public VectorSearchDistanceStep distanceOf(String column, Vector vector) {
+ return new VectorSearchDistanceStep() {
+ @Override
+ public PostgresQueryExpression using(ScoringFunction scoringFunction) {
+ return new PgSqlImpl.DistanceFunction(QueryExpression.column(column), scoringFunction, vector);
+ }
+
+ };
+ }
+ };
+ }
+
+ /**
+ * Postgres-specific {@link Criteria} implementation that provides access to Postgres-specific operators and
+ * functions.
+ */
+ public static class PgCriteria extends Criteria {
+
+ protected PgCriteria(QueryExpression source) {
+ super(source);
+ }
+
+ protected PgCriteria(@Nullable Criteria previous, Combinator combinator, List group,
+ @Nullable QueryExpression queryExpression, @Nullable Comparator comparator, @Nullable Object value) {
+ super(previous, combinator, group, queryExpression, null, comparator, value);
+ }
+
+ protected PgCriteria(@Nullable Criteria previous, Combinator combinator, List group,
+ @Nullable QueryExpression queryExpression, @Nullable Comparator comparator, @Nullable Object value,
+ boolean ignoreCase) {
+ super(previous, combinator, group, queryExpression, null, comparator, value, ignoreCase);
+ }
+
+ /**
+ * Entry point for creating a {@link PgCriteria.PostgresCriteriaStep} for the given column (or property specified as
+ * property path).
+ *
+ * @param column
+ * @return
+ */
+ public static PgCriteria.PostgresCriteriaStep where(String column) {
+ return new PgSqlImpl.DefaultPostgresCriteriaStep(column);
+ }
+
+ /**
+ * Entry point for creating a {@link PgCriteria.PostgresCriteriaStep} for the given column (or property specified as
+ * property path) that shall be wrapped by a wrapping function to process the column contents before its use in a
+ * downstream condition or expression.
+ *
+ * @param column
+ * @param wrappingFunction
+ * @return
+ */
+ public static PgCriteria.PostgresCriteriaStep where(String column,
+ Function wrappingFunction) {
+
+ return where(new CriteriaSources.Column(column), wrappingFunction);
+ }
+
+ /**
+ * Entry point for creating a {@link PgCriteria.PostgresCriteriaStep} for the given {@link QueryExpression
+ * expression}.
+ *
+ * @param expression
+ * @return
+ */
+ public static PgCriteria.PostgresCriteriaStep where(QueryExpression expression) {
+ return new PgSqlImpl.DefaultPostgresCriteriaStep(expression);
+ }
+
+ /**
+ * Entry point for creating a {@link PgCriteria.PostgresCriteriaStep} for the given {@link QueryExpression} that
+ * shall be wrapped by a wrapping function to process the column contents before its use in a downstream condition
+ * or expression.
+ *
+ * @param expression
+ * @param wrappingFunction
+ * @return
+ */
+ public static PgCriteria.PostgresCriteriaStep where(QueryExpression expression,
+ Function wrappingFunction) {
+
+ PgSqlImpl.DefaultOperators functions = new PgSqlImpl.DefaultOperators(expression);
+ return where(wrappingFunction.apply(functions));
+ }
+
+ @Override
+ public PgCriteria.PostgresCriteriaStep and(String column) {
+
+ Assert.hasText(column, "Column name must not be null or empty");
+
+ return and(QueryExpression.column(column));
+ }
+
+ public PgCriteria.PostgresCriteriaStep and(String column, Function wrappingFunction) {
+
+ Assert.hasText(column, "Column name must not be null or empty");
+
+ return and(wrappingFunction.apply(new PgSqlImpl.DefaultOperators(QueryExpression.column(column))));
+ }
+
+ @Override
+ public PgCriteria.PostgresCriteriaStep and(QueryExpression expression) {
+
+ Assert.notNull(expression, "Query expression must not be null");
+
+ return createStep(expression, Combinator.AND);
+ }
+
+ @Override
+ public PgCriteria.PostgresCriteriaStep or(String column) {
+
+ Assert.hasText(column, "Column name must not be null or empty");
+
+ return or(QueryExpression.column(column));
+ }
+
+ public PgCriteria.PostgresCriteriaStep or(String column, Function wrappingFunction) {
+
+ Assert.hasText(column, "Column name must not be null or empty");
+
+ return or(wrappingFunction.apply(new PgSqlImpl.DefaultOperators(QueryExpression.column(column))));
+ }
+
+ @Override
+ public PgCriteria.PostgresCriteriaStep or(QueryExpression expression) {
+
+ Assert.notNull(expression, "Query expression must not be null");
+
+ return createStep(expression, Combinator.OR);
+ }
+
+ private PgSqlImpl.DefaultPostgresCriteriaStep createStep(QueryExpression expression, Combinator combinator) {
+
+ return new PgSqlImpl.DefaultPostgresCriteriaStep(expression) {
+
+ @Override
+ protected PgCriteria createCriteria(Comparator comparator, @Nullable Object value) {
+ return new PgCriteria(PgCriteria.this, combinator, Collections.emptyList(), expression, comparator, value);
+ }
+
+ @Override
+ protected PgCriteria createCriteria(QueryExpression queryExpression) {
+ return new PgCriteria(PgCriteria.this, combinator, Collections.emptyList(), expression, null, null);
+ }
+ };
+ }
+
+ /**
+ * Interface providing a fluent API builder methods to build a {@link Criteria}.
+ */
+ public interface PostgresCriteriaStep extends CriteriaStep {
+
+ /**
+ * Array criteria steps.
+ *
+ * @return
+ */
+ PostgresArrayCriteriaStep arrays();
+
+ /**
+ * JSON criteria steps.
+ *
+ * @return
+ */
+ PgCriteria json(Function criteriaFunction);
+
+ /**
+ * Consider the previous expression as expression evaluating a boolean result.
+ *
+ * @return
+ */
+ PgCriteria asBoolean();
+
+ @Override
+ PgCriteria is(Object value);
+
+ @Override
+ PgCriteria not(Object value);
+
+ @Override
+ PgCriteria in(Object... values);
+
+ @Override
+ PgCriteria in(Collection> values);
+
+ @Override
+ PgCriteria notIn(Object... values);
+
+ @Override
+ PgCriteria notIn(Collection> values);
+
+ @Override
+ PgCriteria between(Object begin, Object end);
+
+ @Override
+ PgCriteria notBetween(Object begin, Object end);
+
+ @Override
+ PgCriteria lessThan(Object value);
+
+ @Override
+ PgCriteria lessThanOrEquals(Object value);
+
+ @Override
+ PgCriteria greaterThan(Object value);
+
+ @Override
+ PgCriteria greaterThanOrEquals(Object value);
+
+ @Override
+ PgCriteria like(Object value);
+
+ @Override
+ PgCriteria notLike(Object value);
+
+ @Override
+ PgCriteria isNull();
+
+ @Override
+ PgCriteria isNotNull();
+
+ @Override
+ PgCriteria isTrue();
+
+ @Override
+ PgCriteria isFalse();
+ }
+
+ /**
+ * Fluent Postgres-specific Array criteria API providing access to Array operators and functions.
+ */
+ public interface PostgresArrayCriteriaStep {
+
+ /**
+ * Does the first array contain the second, that is, does each element appearing in the second array equal some
+ * element of the first array using {@code @>}.
+ *
+ * @param expression
+ * @return
+ */
+ PgCriteria contains(QueryExpression expression);
+
+ /**
+ * Does the first array contain the second, that is, does each element appearing in the second array equal some
+ * element of the first array using {@code @>}.
+ *
+ * @param column
+ * @property
+ */
+ default PgCriteria contains(String column) {
+ return contains(QueryExpression.column(column));
+ }
+
+ /**
+ * Does the first array contain {@code values}, that is, does each element appearing in the second array equal
+ * some element of the first array using {@code @>}.
+ *
+ * @param values
+ * @property
+ */
+ default PgCriteria contains(Object... values) {
+ return contains(PgSqlImpl.ArrayExpression.expressionOrWrap(values));
+ }
+
+ /**
+ * Do the arrays overlap, that is, have any elements in common using {@code &&}.
+ *
+ * @param expression
+ * @return
+ */
+ PgCriteria overlaps(QueryExpression expression);
+
+ /**
+ * Do the arrays overlap, that is, have any elements in common using {@code &&}.
+ *
+ * @param column
+ * @return
+ */
+ default PgCriteria overlaps(String column) {
+ return overlaps(QueryExpression.column(column));
+ }
+
+ /**
+ * Do the arrays overlap, that is, have any elements in common using {@code &&}.
+ *
+ * @param values
+ * @property
+ */
+ default PgCriteria overlaps(Object... values) {
+ return overlaps(PgSqlImpl.ArrayExpression.expressionOrWrap(values));
+ }
+
+ }
+
+ /**
+ * Fluent Postgres-specific JSON criteria API providing access to JSON operators and functions.
+ */
+ public interface PostgresJsonCriteriaStep {
+
+ /**
+ * {@code ?} operator for JSON exists.
+ *
+ * @param column
+ * @return
+ */
+ default PgCriteria exists(String column) {
+ return exists(QueryExpression.column(column));
+ }
+
+ /**
+ * {@code ?} operator for JSON exists.
+ *
+ * @param expression
+ * @return
+ */
+ PgCriteria exists(QueryExpression expression);
+
+ /**
+ * {@code ?} operator for JSON exists.
+ *
+ * @param value
+ * @return
+ */
+ default PgCriteria exists(Object value) {
+ return exists(new PgSqlImpl.ValueExpression(value));
+ }
+
+ /**
+ * {@code ?|} operator for JSON contains.
+ *
+ * @param value
+ * @return
+ */
+ default PgCriteria contains(String field) {
+ return contains(QueryExpression.column(field));
+ }
+
+ /**
+ * {@code ?|} operator for JSON contains.
+ *
+ * @param value
+ * @return
+ */
+ PgCriteria contains(Object value);
+
+ /**
+ * {@code ?&} operator for JSON contains all.
+ *
+ * @param values
+ * @return
+ */
+ default PgCriteria containsAll(Object... values) {
+ return containsAll(Arrays.asList(values));
+ }
+
+ /**
+ * {@code ?&} operator for JSON contains all.
+ *
+ * @param values
+ * @return
+ */
+ PgCriteria containsAll(Collection