JpqlQueryBuilder.java
/*
* Copyright 2024-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.jpa.repository.query;
import static org.springframework.data.jpa.repository.query.QueryTokens.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.util.Predicates;
import org.springframework.lang.CheckReturnValue;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* A Domain-Specific Language to build JPQL queries using Java code.
*
* @author Mark Paluch
* @author Choi Wang Gyu
*/
@SuppressWarnings("JavadocDeclaration")
public final class JpqlQueryBuilder {
private JpqlQueryBuilder() {}
/**
* Create an {@link Entity} from the given {@link JpaEntityMetadata}.
*
* @param from the entity type to select from.
* @return
*/
public static Entity entity(JpaEntityMetadata<?> from) {
return new Entity(from.getJavaType(), from.getEntityName(),
getAlias(from.getJavaType().getSimpleName(), Predicates.isTrue(), () -> "r"));
}
/**
* Create a {@link Join INNER JOIN}.
*
* @param origin the selection origin (a join or the entity itself) to select from.
* @param path
* @return
*/
public static Join innerJoin(Origin origin, String path) {
return new Join(origin, "INNER JOIN", path);
}
/**
* Create a {@link Join LEFT JOIN}.
*
* @param origin the selection origin (a join or the entity itself) to select from.
* @param path
* @return
*/
public static Join leftJoin(Origin origin, String path) {
return new Join(origin, "LEFT JOIN", path);
}
/**
* Start building a {@link Select} statement by selecting {@link Entity from}.
*
* @param from the entity source to select from.
* @return a new select builder.
*/
public static SelectStep selectFrom(Entity from) {
return new SelectStep() {
boolean distinct = false;
@Override
public SelectStep distinct() {
distinct = true;
return this;
}
@Override
public Select entity() {
return new Select(postProcess(new EntitySelection(from)), from);
}
@Override
public Select count() {
return new Select(new CountSelection(from, distinct), from);
}
@Override
public Select instantiate(String resultType, Collection<? extends JpqlQueryBuilder.Expression> paths) {
return new Select(postProcess(new ConstructorExpression(resultType, new Multiselect(from, paths))), from);
}
@Override
public Select select(Collection<? extends JpqlQueryBuilder.Expression> paths) {
return new Select(postProcess(new Multiselect(from, paths)), from);
}
@Override
public Select select(Selection selection) {
return new Select(postProcess(selection), from);
}
Selection postProcess(Selection selection) {
return distinct ? new DistinctSelection(selection) : selection;
}
};
}
private static String getAlias(String from, java.util.function.Predicate<String> predicate,
Supplier<String> fallback) {
char c = from.toLowerCase(Locale.ROOT).charAt(0);
String string = Character.toString(c);
if (Character.isJavaIdentifierPart(c) && predicate.test(string)) {
return string;
}
return fallback.get();
}
/**
* Invoke a {@literal function} with the given {@code arguments}.
*
* @param function function name.
* @param arguments function arguments.
* @return an expression representing a function call.
*/
public static Expression function(String function, Expression... arguments) {
return new FunctionExpression(function, Arrays.asList(arguments));
}
/**
* Nest the given {@link Predicate}.
*
* @param predicate
* @return
*/
public static Predicate nested(Predicate predicate) {
return new NestedPredicate(predicate);
}
/**
* Create a qualified expression for a {@link PropertyPath}.
*
* @param source
* @param path
* @return
*/
public static Expression expression(Origin source, PropertyPath path) {
return new PathAndOrigin(path, source, false);
}
/**
* Create a simple expression from a string as-is.
*
* @param expression
* @return
*/
public static Expression expression(String expression) {
Assert.hasText(expression, "Expression must not be empty or null");
return new LiteralExpression(expression);
}
/**
* Create a simple numeric literal.
*
* @param literal
* @return
*/
public static Expression literal(Number literal) {
return new LiteralExpression(literal.toString());
}
/**
* Create a simple literal from a string by quoting it.
*
* @param literal
* @return
*/
public static Expression literal(String literal) {
return new StringLiteralExpression(literal);
}
/**
* A parameter placeholder.
*
* @param parameter
* @return
*/
public static Expression parameter(String parameter) {
Assert.hasText(parameter, "Parameter must not be empty or null");
return new ParameterExpression(new ParameterPlaceholder(parameter));
}
/**
* A parameter placeholder.
*
* @param placeholder the placeholder to use.
* @return
*/
public static Expression parameter(ParameterPlaceholder placeholder) {
return new ParameterExpression(placeholder);
}
/**
* Create a new ordering expression.
*
* @param sortExpression
* @return
* @since 4.0
*/
public static Expression orderBy(Expression sortExpression) {
return new OrderExpression(sortExpression, null, Sort.NullHandling.NATIVE);
}
/**
* Create a new ordering expression.
*
* @param sortExpression
* @param order
* @return
*/
public static Expression orderBy(Expression sortExpression, Sort.Order order) {
return new OrderExpression(sortExpression, order.getDirection(), order.getNullHandling());
}
/**
* Create a new ordering expression.
*
* @param sortExpression
* @param direction
* @return
* @since 4.0
*/
public static Expression orderBy(Expression sortExpression, Sort.Direction direction) {
return new OrderExpression(sortExpression, direction, Sort.NullHandling.NATIVE);
}
/**
* Start building a {@link Predicate WHERE predicate} by providing the right-hand side.
*
* @param source
* @param path
* @return
*/
public static WhereStep where(Origin source, PropertyPath path) {
return where(expression(source, path));
}
/**
* Start building a {@link Predicate WHERE predicate} by providing the right-hand side.
*
* @param rhs
* @return
*/
public static WhereStep where(Expression rhs) {
return new WhereStep() {
@Override
public Predicate between(Expression lower, Expression upper) {
return new BetweenPredicate(rhs, lower, upper);
}
@Override
public Predicate gt(Expression value) {
return new OperatorPredicate(rhs, ">", value);
}
@Override
public Predicate gte(Expression value) {
return new OperatorPredicate(rhs, ">=", value);
}
@Override
public Predicate lt(Expression value) {
return new OperatorPredicate(rhs, "<", value);
}
@Override
public Predicate lte(Expression value) {
return new OperatorPredicate(rhs, "<=", value);
}
@Override
public Predicate isNull() {
return new LhsPredicate(rhs, "IS NULL");
}
@Override
public Predicate isNotNull() {
return new LhsPredicate(rhs, "IS NOT NULL");
}
@Override
public Predicate isTrue() {
return new LhsPredicate(rhs, "= TRUE");
}
@Override
public Predicate isFalse() {
return new LhsPredicate(rhs, "= FALSE");
}
@Override
public Predicate isEmpty() {
return new LhsPredicate(rhs, "IS EMPTY");
}
@Override
public Predicate isNotEmpty() {
return new LhsPredicate(rhs, "IS NOT EMPTY");
}
@Override
public Predicate in(Expression value) {
return new InPredicate(rhs, "IN", value);
}
@Override
public Predicate notIn(Expression value) {
return new InPredicate(rhs, "NOT IN", value);
}
@Override
public Predicate memberOf(Expression value) {
return new MemberOfPredicate(rhs, "MEMBER OF", value);
}
@Override
public Predicate notMemberOf(Expression value) {
return new MemberOfPredicate(rhs, "NOT MEMBER OF", value);
}
@Override
public Predicate like(Expression value, String escape) {
return new LikePredicate(rhs, "LIKE", value, escape);
}
@Override
public Predicate notLike(Expression value, String escape) {
return new LikePredicate(rhs, "NOT LIKE", value, escape);
}
@Override
public Predicate eq(Expression value) {
return new OperatorPredicate(rhs, "=", value);
}
@Override
public Predicate neq(Expression value) {
return new OperatorPredicate(rhs, "!=", value);
}
};
}
public static @Nullable Predicate and(List<Predicate> intermediate) {
Predicate predicate = null;
for (Predicate other : intermediate) {
if (predicate == null) {
predicate = other;
} else {
predicate = predicate.and(other);
}
}
return predicate;
}
public static @Nullable Predicate or(List<Predicate> intermediate) {
Predicate predicate = null;
for (Predicate other : intermediate) {
if (predicate == null) {
predicate = other;
} else {
predicate = predicate.or(other);
}
}
return predicate;
}
/**
* Fluent interface to build a {@link Select}.
*/
public interface SelectStep {
/**
* Apply {@code DISTINCT}.
*/
@CheckReturnValue
SelectStep distinct();
/**
* Select the entity.
*/
@CheckReturnValue
Select entity();
/**
* Select the count.
*/
@CheckReturnValue
Select count();
/**
* Provide a constructor expression to instantiate {@code resultType}. Operates on the underlying {@link Entity
* FROM}.
*
* @param resultType
* @param paths
* @return
*/
@CheckReturnValue
default Select instantiate(Class<?> resultType, Collection<? extends JpqlQueryBuilder.Expression> paths) {
return instantiate(resultType.getName(), paths);
}
/**
* Provide a constructor expression to instantiate {@code resultType}.
*
* @param resultType
* @param paths
* @returninstanti
*/
@CheckReturnValue
Select instantiate(String resultType, Collection<? extends JpqlQueryBuilder.Expression> paths);
/**
* Specify a multi-select.
*
* @param paths
* @return
*/
@CheckReturnValue
Select select(Collection<? extends JpqlQueryBuilder.Expression> paths);
/**
* Select a single attribute.
*
* @param path
* @return
*/
@CheckReturnValue
default Select select(JpqlQueryBuilder.PathExpression path) {
return select(List.of(path));
}
/**
* Select a single attribute.
*
* @param selection
* @return
*/
@CheckReturnValue
Select select(Selection selection);
}
public interface Selection {
String render(RenderContext context);
}
/**
* {@code DISTINCT} wrapper.
*
* @param selection
*/
record DistinctSelection(Selection selection) implements Selection {
@Override
public String render(RenderContext context) {
return "DISTINCT %s".formatted(selection.render(context));
}
@Override
public String toString() {
return render(RenderContext.EMPTY);
}
}
static PathAndOrigin path(Origin origin, String path) {
if (origin instanceof Entity entity) {
PropertyPath from = PropertyPath.from(path, entity.entityClass);
return new PathAndOrigin(from, entity, false);
}
if (origin instanceof Join join) {
Origin parent = join.source;
List<String> segments = new ArrayList<>();
segments.add(join.path);
while (!(parent instanceof Entity)) {
if (parent instanceof Join parentJoin) {
parent = parentJoin.source;
segments.add(parentJoin.path);
} else {
parent = null;
}
}
Collections.reverse(segments);
segments.add(path);
PathAndOrigin joinedPath = path(parent, StringUtils.collectionToDelimitedString(segments, "."));
return new PathAndOrigin(joinedPath.path().getLeafProperty(), origin, false);
}
throw new IllegalStateException("���� Unsupported origin type: " + origin);
}
/**
* Entity selection.
*
* @param source
*/
record EntitySelection(Entity source) implements Selection, Expression {
@Override
public String render(RenderContext context) {
return context.getAlias(source);
}
@Override
public String toString() {
return render(RenderContext.EMPTY);
}
}
/**
* {@code COUNT(���)} selection.
*
* @param source
* @param distinct
*/
record CountSelection(Entity source, boolean distinct) implements Selection {
@Override
public String render(RenderContext context) {
return "COUNT(%s%s)".formatted(distinct ? "DISTINCT " : "", context.getAlias(source));
}
@Override
public String toString() {
return render(RenderContext.EMPTY);
}
}
/**
* Expression selection.
*
* @param resultType
* @param multiselect
*/
record ConstructorExpression(String resultType, Multiselect multiselect) implements Selection, Expression {
@Override
public String render(RenderContext context) {
return "new %s(%s)".formatted(resultType, multiselect.render(new ConstructorContext(context)));
}
@Override
public String toString() {
return render(RenderContext.EMPTY);
}
}
/**
* Multi-select selecting one or many property paths.
*
* @param source
* @param paths
*/
record Multiselect(Origin source, Collection<? extends JpqlQueryBuilder.Expression> paths) implements Selection {
@Override
public String render(RenderContext context) {
StringBuilder builder = new StringBuilder();
for (Expression path : paths) {
if (!builder.isEmpty()) {
builder.append(", ");
}
builder.append(path.render(context));
if (!context.isConstructorContext() && path instanceof AliasedExpression ae) {
builder.append(" ").append(ae.getAlias());
}
}
return builder.toString();
}
@Override
public String toString() {
return render(RenderContext.EMPTY);
}
}
/**
* Interface specifying a predicate or expression that can be rendered to {@code String}.
*/
public interface Renderable {
/**
* Render the predicate or expression given {@link RenderContext}.
*
* @param context
* @return
*/
String render(RenderContext context);
}
/**
* {@code WHERE} predicate.
*/
public interface Predicate extends Renderable {
/**
* {@code OR}-concatenate this predicate with {@code other}.
*
* @param other
* @return a composed predicate combining this and {@code other} using the OR operator.
*/
@Contract("_ -> new")
@CheckReturnValue
default Predicate or(Predicate other) {
return new OrPredicate(this, other);
}
/**
* {@code AND}-concatenate this predicate with {@code other}.
*
* @param other
* @return a composed predicate combining this and {@code other} using the AND operator.
*/
@Contract("_ -> new")
@CheckReturnValue
default Predicate and(Predicate other) { // don't like the structuring of this and the nest() thing
return new AndPredicate(this, other);
}
/**
* Wrap this predicate with parenthesis {@code (���)} to nest it without affecting AND/OR concatenation precedence.
*
* @return a nested variant of this predicate.
*/
@Contract("-> new")
@CheckReturnValue
default Predicate nest() {
return new NestedPredicate(this);
}
}
/**
* Interface specifying an expression that can be rendered to {@code String}.
*/
public interface Expression extends Renderable {
/**
* Create an {@link AliasedExpression} with the given {@code alias}. If the expression is already aliased, the
* previous alias is discarded and replaced with the new one.
*
* @param alias
* @return
*/
default AliasedExpression as(String alias) {
if (this instanceof DefaultAliasedExpression de) {
return new DefaultAliasedExpression(de.delegate, alias);
}
return new DefaultAliasedExpression(this, alias);
}
}
/**
* Aliased expression.
*
* @since 4.0
*/
public interface AliasedExpression extends Expression {
/**
* @return the expression alias.
*/
String getAlias();
}
record DefaultAliasedExpression(Expression delegate, String alias) implements AliasedExpression {
@Override
public String render(RenderContext context) {
return delegate.render(context);
}
@Override
public String getAlias() {
return alias();
}
@Override
public String toString() {
return render(RenderContext.EMPTY);
}
}
/**
* Extension to {@link Expression} that contains a {@link PropertyPath}. Typically used to represent a selection
* expression or an expression used within sorting or {@code WHERE} clauses.
*/
public interface PathExpression extends Expression {
/**
* @return the associated {@link PropertyPath}.
*/
PropertyPath getPropertyPath();
}
/**
* {@code SELECT} statement.
*/
public static class Select extends AbstractJpqlQuery {
private final Selection selection;
private final Entity entity;
private final Map<String, Join> joins = new LinkedHashMap<>();
private final List<Expression> orderBy = new ArrayList<>();
private Select(Selection selection, Entity entity) {
this.selection = selection;
this.entity = entity;
}
/**
* Append a join to this select.
*
* @param join
* @return
*/
@Contract("_ -> this")
public Select join(Join join) {
if (join.source() instanceof Join parent) {
join(parent);
}
this.joins.put(join.joinType() + "_" + join.getName() + "_" + join.path(), join);
return this;
}
/**
* Append an order-by expression to this select.
*
* @param orderBy
* @return
*/
@Contract("_ -> this")
public Select orderBy(Expression orderBy) {
this.orderBy.add(orderBy);
return this;
}
@Override
String render() {
Map<Origin, String> aliases = new LinkedHashMap<>();
aliases.put(entity, entity.alias);
RenderContext renderContext = new RenderContext(aliases);
StringBuilder where = new StringBuilder();
StringBuilder orderby = new StringBuilder();
StringBuilder result = new StringBuilder(
"SELECT %s FROM %s %s".formatted(selection.render(renderContext), entity.getName(), entity.getAlias()));
if (getWhere() != null) {
where.append(" WHERE ").append(getWhere().render(renderContext));
}
if (!orderBy.isEmpty()) {
StringBuilder builder = new StringBuilder();
for (Expression order : orderBy) {
if (!builder.isEmpty()) {
builder.append(", ");
}
builder.append(order.render(renderContext));
}
orderby.append(" ORDER BY ").append(builder);
}
aliases.keySet().forEach(key -> {
if (key instanceof Join js) {
join(js);
}
});
for (Join join : joins.values()) {
result.append(" ").append(join.joinType()).append(" ").append(renderContext.getAlias(join.source())).append(".")
.append(join.path()).append(" ").append(renderContext.getAlias(join));
}
result.append(where).append(orderby);
return result.toString();
}
}
/**
* Abstract base class for JPQL queries.
*/
public static abstract class AbstractJpqlQuery {
private @Nullable Predicate where;
public AbstractJpqlQuery where(Predicate predicate) {
this.where = predicate;
return this;
}
public @Nullable Predicate getWhere() {
return where;
}
abstract String render();
@Override
public String toString() {
return render();
}
}
record OrderExpression(Expression sortExpression, @org.springframework.lang.Nullable Sort.Direction direction,
Sort.NullHandling nullHandling) implements Expression {
@Override
public String render(RenderContext context) {
StringBuilder builder = new StringBuilder();
builder.append(sortExpression.render(context));
if (direction != null) {
builder.append(" ");
builder.append(direction.isDescending() ? TOKEN_DESC : TOKEN_ASC);
if (nullHandling == Sort.NullHandling.NULLS_FIRST) {
builder.append(" NULLS FIRST");
} else if (nullHandling == Sort.NullHandling.NULLS_LAST) {
builder.append(" NULLS LAST");
}
}
return builder.toString();
}
}
/**
* Context used during rendering.
*/
public static class RenderContext {
public static final RenderContext EMPTY = new RenderContext(Collections.emptyMap()) {
@Override
public String getAlias(Origin source) {
return "";
}
};
private final Map<Origin, String> aliases;
private int counter;
RenderContext(Map<Origin, String> aliases) {
this.aliases = aliases;
}
/**
* Obtain an alias for {@link Origin}. Unknown selection origins are associated with the enclosing statement if they
* are used for the first time.
*
* @param source
* @return
*/
public String getAlias(Origin source) {
return aliases.computeIfAbsent(source, it -> JpqlQueryBuilder.getAlias(source.getName(),
s -> !aliases.containsValue(s), () -> "join_" + (counter++)));
}
/**
* Prefix {@code fragment} with the alias for {@link Origin}. Unknown selection origins are associated with the
* enclosing statement if they are used for the first time.
*
* @param source
* @return
*/
public String prefixWithAlias(Origin source, String fragment) {
String alias = getAlias(source);
return ObjectUtils.isEmpty(source) ? fragment : alias + "." + fragment;
}
public boolean isConstructorContext() {
return false;
}
}
static class ConstructorContext extends RenderContext {
ConstructorContext(RenderContext rootContext) {
super(rootContext.aliases);
}
@Override
public boolean isConstructorContext() {
return true;
}
}
/**
* An origin that is used to select data from. selection origins are used with paths to define where a path is
* anchored.
*/
public interface Origin {
/**
* Returns the simple name of the origin (e.g. {@link Class#getSimpleName()} or JOIN path name).
*
* @return the simple name of the origin (e.g. {@link Class#getSimpleName()})
*/
String getName();
}
/**
* The root entity.
*/
public static final class Entity implements Origin {
private final Class<?> entityClass;
private final String entity;
private final String alias;
/**
* @param entityClass entity class.
* @param entity entity name (as in {@code @Entity(���)}).
* @param alias alias to use.
*/
Entity(Class<?> entityClass, String entity, String alias) {
this.entityClass = entityClass;
this.entity = entity;
this.alias = alias;
}
@Override
public String getName() {
return entity;
}
public String getAlias() {
return alias;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || obj.getClass() != this.getClass()) {
return false;
}
var that = (Entity) obj;
return Objects.equals(this.entity, that.entity) && Objects.equals(this.entityClass, that.entityClass)
&& Objects.equals(this.alias, that.alias);
}
@Override
public int hashCode() {
return Objects.hash(entity, entityClass, alias);
}
@Override
public String toString() {
return "Entity[" + "entity=" + entity + ", " + "className=" + entityClass.getName() + ", " + "alias=" + alias
+ ']';
}
}
/**
* A joined entity or element collection.
*/
public static final class Join implements Origin, Expression {
private final Origin source;
private final String joinType;
private final String path;
/**
* @param source
* @param joinType
* @param path
*/
Join(Origin source, String joinType, String path) {
this.source = source;
this.joinType = joinType;
this.path = path;
}
@Override
public String getName() {
return path;
}
@Override
public String render(RenderContext context) {
return "%s %s %s".formatted(joinType, context.getAlias(source), path);
}
public Origin source() {
return source;
}
public String joinType() {
return joinType;
}
public String path() {
return path;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || obj.getClass() != this.getClass()) {
return false;
}
var that = (Join) obj;
return Objects.equals(this.source, that.source) && Objects.equals(this.joinType, that.joinType)
&& Objects.equals(this.path, that.path);
}
@Override
public int hashCode() {
return Objects.hash(source, joinType, path);
}
@Override
public String toString() {
return "Join[" + "source=" + source + ", " + "joinType=" + joinType + ", " + "path=" + path + ']';
}
}
/**
* Fluent interface to build a {@link Predicate}.
*/
public interface WhereStep {
/**
* Create a {@code BETWEEN ��� AND ���} predicate.
*
* @param lower lower boundary.
* @param upper upper boundary.
* @return
*/
Predicate between(Expression lower, Expression upper);
/**
* Create a greater {@code > ���} predicate.
*
* @param value the comparison value.
* @return
*/
Predicate gt(Expression value);
/**
* Create a greater-or-equals {@code >= ���} predicate.
*
* @param value the comparison value.
* @return
*/
Predicate gte(Expression value);
/**
* Create a less {@code < ���} predicate.
*
* @param value the comparison value.
* @return
*/
Predicate lt(Expression value);
/**
* Create a less-or-equals {@code <= ���} predicate.
*
* @param value the comparison value.
* @return
*/
Predicate lte(Expression value);
/**
* Create a {@code IS NULL} predicate.
*
* @return
*/
Predicate isNull();
/**
* Create a {@code IS NOT NULL} predicate.
*
* @return
*/
Predicate isNotNull();
/**
* Create a {@code IS TRUE} predicate.
*
* @return
*/
Predicate isTrue();
/**
* Create a {@code IS FALSE} predicate.
*
* @return
*/
Predicate isFalse();
/**
* Create a {@code IS EMPTY} predicate.
*
* @return
*/
Predicate isEmpty();
/**
* Create a {@code IS NOT EMPTY} predicate.
*
* @return
*/
Predicate isNotEmpty();
/**
* Create a {@code IN} predicate.
*
* @param value
* @return
*/
Predicate in(Expression value);
/**
* Create a {@code NOT IN} predicate.
*
* @param value
* @return
*/
Predicate notIn(Expression value);
/**
* Create a {@code MEMBER OF <collection>} predicate.
*
* @param value
* @return
*/
Predicate memberOf(Expression value);
/**
* Create a {@code NOT MEMBER OF <collection>} predicate.
*
* @param value
* @return
*/
Predicate notMemberOf(Expression value);
default Predicate like(String value, String escape) {
return like(expression(value), escape);
}
/**
* Create a {@code LIKE ��� ESCAPE} predicate.
*
* @param value
* @return
*/
Predicate like(Expression value, String escape);
/**
* Create a {@code NOT LIKE ��� ESCAPE} predicate.
*
* @param value
* @return
*/
Predicate notLike(Expression value, String escape);
/**
* Create a {@code =} (equals) predicate.
*
* @param value
* @return
*/
Predicate eq(Expression value);
/**
* Create a {@code <>} (not equals) predicate.
*
* @param value
* @return
*/
Predicate neq(Expression value);
}
record LiteralExpression(String expression) implements Expression {
@Override
public String render(RenderContext context) {
return expression;
}
@Override
public String toString() {
return render(RenderContext.EMPTY);
}
}
record StringLiteralExpression(String literal) implements Expression {
@Override
public String render(RenderContext context) {
return "'%s'".formatted(literal.replaceAll("'", "''"));
}
public String raw() {
return literal;
}
@Override
public String toString() {
return render(RenderContext.EMPTY);
}
}
record ParameterExpression(ParameterPlaceholder parameter) implements Expression {
@Override
public String render(RenderContext context) {
return parameter.placeholder;
}
@Override
public String toString() {
return render(RenderContext.EMPTY);
}
}
record FunctionExpression(String function, List<Expression> arguments) implements Expression {
@Override
public String render(RenderContext context) {
StringBuilder builder = new StringBuilder();
for (Expression argument : arguments) {
if (!builder.isEmpty()) {
builder.append(", ");
}
builder.append(argument.render(context));
}
return "%s(%s)".formatted(function, builder);
}
@Override
public String toString() {
return render(RenderContext.EMPTY);
}
}
record OperatorPredicate(Expression path, String operator, Expression predicate) implements Predicate {
@Override
public String render(RenderContext context) {
return "%s %s %s".formatted(path.render(context), operator, predicate.render(context));
}
@Override
public String toString() {
return render(RenderContext.EMPTY);
}
}
record MemberOfPredicate(Expression path, String operator, Expression predicate) implements Predicate {
@Override
public String render(RenderContext context) {
return "%s %s %s".formatted(predicate.render(context), operator, path.render(context));
}
@Override
public String toString() {
return render(RenderContext.EMPTY);
}
}
record LhsPredicate(Expression path, String predicate) implements Predicate {
@Override
public String render(RenderContext context) {
return "%s %s".formatted(path.render(context), predicate);
}
@Override
public String toString() {
return render(RenderContext.EMPTY);
}
}
record BetweenPredicate(Expression path, Expression lower, Expression upper) implements Predicate {
@Override
public String render(RenderContext context) {
return "%s BETWEEN %s AND %s".formatted(path.render(context), lower.render(context), upper.render(context));
}
@Override
public String toString() {
return render(RenderContext.EMPTY);
}
}
record LikePredicate(Expression left, String operator, Expression right, String escape) implements Predicate {
@Override
public String render(RenderContext context) {
return "%s %s %s ESCAPE '%s'".formatted(left.render(context), operator, right.render(context), escape);
}
@Override
public String toString() {
return render(RenderContext.EMPTY);
}
}
record InPredicate(Expression path, String operator, Expression predicate) implements Predicate {
@Override
public String render(RenderContext context) {
Expression predicate = this.predicate;
String rendered = predicate.render(context);
return (hasParenthesis(rendered) ? "%s %s %s" : "%s %s (%s)").formatted(path.render(context), operator, rendered);
}
@Override
public String toString() {
return render(RenderContext.EMPTY);
}
private static boolean hasParenthesis(String str) {
return str.startsWith("(") && str.endsWith(")");
}
}
record AndPredicate(Predicate left, Predicate right) implements Predicate {
@Override
public String render(RenderContext context) {
return "%s AND %s".formatted(left.render(context), right.render(context));
}
@Override
public String toString() {
return render(RenderContext.EMPTY);
}
}
record OrPredicate(Predicate left, Predicate right) implements Predicate {
@Override
public String render(RenderContext context) {
return "%s OR %s".formatted(left.render(context), right.render(context));
}
@Override
public String toString() {
return render(RenderContext.EMPTY);
}
}
record NestedPredicate(Predicate delegate) implements Predicate {
@Override
public String render(RenderContext context) {
return "(%s)".formatted(delegate.render(context));
}
@Override
public String toString() {
return render(RenderContext.EMPTY);
}
}
/**
* Value object capturing a property path and its origin.
*
* @param path
* @param origin
* @param onTheJoin whether the path should target the join itself instead of matching {@link PropertyPath}.
*/
record PathAndOrigin(PropertyPath path, Origin origin,
boolean onTheJoin) implements PathExpression, AliasedExpression {
@Override
public PropertyPath getPropertyPath() {
return path;
}
@Override
public String render(RenderContext context) {
if (path().hasNext() || !onTheJoin()) {
return context.prefixWithAlias(origin(), path().toDotPath());
} else {
return context.getAlias(origin());
}
}
@Override
public String getAlias() {
return path().getSegment();
}
}
/**
* Value object capturing parameter placeholder.
*
* @param placeholder
*/
public record ParameterPlaceholder(String placeholder) {
public ParameterPlaceholder {
Assert.hasText(placeholder, "Placeholder must not be null nor empty");
}
/**
* Factory method to create a parameter placeholder using a parameter {@code index}.
*
* @param index the parameter index.
* @return an indexed parameter placeholder.
*/
public static ParameterPlaceholder indexed(int index) {
return new ParameterPlaceholder("?%s".formatted(index));
}
/**
* Factory method to create a parameter placeholder using a parameter {@code name}.
*
* @param name the parameter name.
* @return a named parameter placeholder.
*/
public static ParameterPlaceholder named(String name) {
Assert.hasText(name, "Placeholder name must not be empty");
return new ParameterPlaceholder(":%s".formatted(name));
}
}
}