Skip to content

Commit

Permalink
Create API for more flexible queries
Browse files Browse the repository at this point in the history
Signed-off-by: Niels Thykier <[email protected]>
  • Loading branch information
Niels Thykier committed Jun 4, 2021
1 parent 96815db commit 93d601e
Show file tree
Hide file tree
Showing 18 changed files with 766 additions and 213 deletions.
13 changes: 13 additions & 0 deletions src/main/java/org/dcsa/core/extendedrequest/QueryField.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.dcsa.core.extendedrequest;

import org.springframework.data.domain.Sort;
import org.springframework.data.relational.core.sql.Column;
import org.springframework.data.relational.core.sql.OrderByField;

import java.lang.reflect.Field;

Expand All @@ -19,7 +21,18 @@ default boolean isSelectable() {
default String getDatePattern() {
return null;
}
default String getFieldPath() {
return null;
}
default Field getCombinedModelField() {
return null;
}

default OrderByField asOrderByField(Sort.Direction direction) {
Column column = getSelectColumn();
if (column == null) {
column = getInternalQueryColumn();
}
return OrderByField.from(column, direction);
}
}
5 changes: 5 additions & 0 deletions src/main/java/org/dcsa/core/extendedrequest/QueryFields.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public static QueryField queryFieldFromFieldWithSelectPrefix(Class<?> combinedMo
selectColumn = internalColumn.as(SqlIdentifier.quoted(namePrefix + selectName));
}
return FieldBackedQueryField.of(
namePrefix + combinedModelField.getName(),
combinedModelField,
internalColumn,
selectColumn,
Expand Down Expand Up @@ -84,6 +85,10 @@ public Column getSelectColumn() {
@Data(staticConstructor = "of")
private static class FieldBackedQueryField implements QueryField {

@NonNull
@Getter
private final String fieldPath;

@NonNull
@Getter
private final Field combinedModelField;
Expand Down
172 changes: 5 additions & 167 deletions src/main/java/org/dcsa/core/extendedrequest/QueryParameterParser.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package org.dcsa.core.extendedrequest;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.dcsa.core.exception.GetException;
import org.dcsa.core.query.DBEntityAnalysis;
import org.dcsa.core.query.impl.ComparisonType;
import org.dcsa.core.query.impl.DelegatingCursorBackedFilterCondition;
import org.dcsa.core.query.impl.InlineableFilterCondition;
import org.springframework.data.annotation.Transient;
import org.springframework.data.r2dbc.dialect.PostgresDialect;
import org.springframework.data.r2dbc.dialect.R2dbcDialect;
import org.springframework.data.relational.core.sql.*;
import org.springframework.r2dbc.core.binding.BindMarker;
import org.springframework.r2dbc.core.binding.Bindings;
import org.springframework.r2dbc.core.binding.MutableBindings;

import java.lang.reflect.Field;
Expand All @@ -21,16 +21,14 @@
import java.time.format.DateTimeParseException;
import java.time.temporal.Temporal;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.dcsa.core.query.impl.QueryGeneratorUtils.andAllFilters;

@RequiredArgsConstructor
public class QueryParameterParser<T> {

public static final FilterCondition EMPTY_CONDITION = InlineableFilterCondition.of(TrueCondition.INSTANCE);

protected final ExtendedParameters extendedParameters;
protected final R2dbcDialect r2dbcDialect;
protected final DBEntityAnalysis<T> dbAnalysis;
Expand Down Expand Up @@ -275,165 +273,5 @@ protected QueryField getQueryFieldFromJSONName(String jsonName) {
return queryField;
}

private static FilterCondition isubstr(Expression lhs, Expression rhsOrig) {
Expression rhs = surroundWithWildCards(rhsOrig);
return r2dbcDialect -> {
if (r2dbcDialect instanceof PostgresDialect) {
return Comparison.create(lhs, "ILIKE", rhs);
}
return Conditions.like(Functions.upper(lhs), Functions.upper(rhs));
};
}

private static FilterCondition iequal(Expression lhs, Expression rhs) {
return InlineableFilterCondition.of(Conditions.isEqual(Functions.upper(lhs), Functions.upper(rhs)));
}

private static Expression surroundWithWildCards(Expression rhs) {
List<Expression> params = new ArrayList<>(3);
params.add(SQL.literalOf("%"));
params.add(rhs);
params.add(SQL.literalOf("%"));
return SimpleFunction.create("CONCAT", params);
}

@Getter
@RequiredArgsConstructor
protected enum ComparisonType {
GT(true, Conditions::isGreater),
GTE(true, Conditions::isGreaterOrEqualTo),
EQ(false, Conditions::isEqual),
LTE(true, Conditions::isLessOrEqualTo),
LT(true, Conditions::isLess),
NEQ(false, Conditions::isNotEqual),
SUBSTR(false, Conditions::like),
IEQ(false, true, QueryParameterParser::iequal),
ISUBSTR(false, true, QueryParameterParser::isubstr),
;

private final boolean requiredOrdering;
private final boolean convertToString;
private final BiFunction<Expression, Expression, FilterCondition> singleValueConverter;

ComparisonType(boolean requiredOrdering, BiFunction<Expression, Expression, Condition> conditionBiFunction) {
this(requiredOrdering, false, (lhs, rhs) -> InlineableFilterCondition.of(conditionBiFunction.apply(lhs, rhs)));
}

public Expression defaultFieldConversion(QueryField queryField) {
boolean needsCast = convertToString;
if (needsCast) {
Class<?> valueType = queryField.getType();
if (valueType.isEnum() || String.class.equals(valueType)) {
needsCast = false;
}
}
if (needsCast) {
Column aliased = queryField.getInternalQueryColumn().as(SqlIdentifier.unquoted("VARCHAR"));
return SimpleFunction.create("CAST", Collections.singletonList(aliased));
}
return queryField.getInternalQueryColumn();
}

public FilterCondition singleNonNullValueCondition(QueryField queryField, Expression expression) {
return singleNonNullValueCondition(defaultFieldConversion(queryField), expression);
}

public FilterCondition singleNonNullValueCondition(Expression field, Expression expression) {
return singleValueConverter.apply(field, expression);
}

public FilterCondition multiValueCondition(QueryField queryField, List<Expression> expressions) {
return multiValueCondition(defaultFieldConversion(queryField), expressions);
}

public FilterCondition multiValueCondition(Expression field, List<Expression> expressions) {
if (expressions.isEmpty()) {
throw new IllegalArgumentException("Right-hand side expression list must be non-empty");
}
if (expressions.size() == 1) {
return singleNonNullValueCondition(field, expressions.get(0));
} else if (this == EQ || this == NEQ) {
if (this == EQ) {
return InlineableFilterCondition.of(Conditions.in(field, expressions));
}
return InlineableFilterCondition.of(Conditions.notIn(field, expressions));
} else {
return andAllFilters(
expressions.stream().map(e -> singleValueConverter.apply(field, e)).collect(Collectors.toList()),
true
);
}
}
}

private static FilterCondition andAllFilters(List<FilterCondition> filters, boolean nestOnMultiple) {
if (filters.isEmpty()) {
return EMPTY_CONDITION;
}
if (filters.size() == 1) {
return filters.get(0);
}
if (filters.stream().allMatch(FilterCondition::isInlineable)) {
return InlineableFilterCondition.of(andAll(filters.stream().map(FilterCondition::computeCondition), nestOnMultiple));
}
return (r2dbcDialect) -> andAll(filters.stream().map(f -> f.computeCondition(r2dbcDialect)), nestOnMultiple);
}

private static Condition andAll(Stream<Condition> conditions, boolean nestOnMultiple) {
return andAll(conditions::iterator, nestOnMultiple);
}

private static Condition andAll(Iterable<Condition> conditions, boolean nestOnMultiple) {
Iterator<Condition> iter = conditions.iterator();
assert iter.hasNext();
Condition con = iter.next();
if (!iter.hasNext()) {
return con;
}
do {
con = con.and(iter.next());
} while (iter.hasNext());
if (!nestOnMultiple) {
return con;
}
return Conditions.nest(con);
}

@RequiredArgsConstructor(staticName = "of")
private static class InlineableFilterCondition implements FilterCondition {
private final Condition condition;

public Condition computeCondition(R2dbcDialect r2dbcDialect) {
return condition;
}

public boolean isInlineable() {
return true;
}
}

@RequiredArgsConstructor(staticName = "of")
private static class DelegatingCursorBackedFilterCondition implements CursorBackedFilterCondition {

private final FilterCondition delegate;

@Getter
private final Map<String, List<String>> cursorParameters;

@Getter
private final Set<QueryField> referencedQueryFields;

@Getter
private final Bindings bindings;

@Override
public Condition computeCondition(R2dbcDialect r2dbcDialect) {
return delegate.computeCondition(r2dbcDialect);
}

@Override
public boolean isInlineable() {
return delegate.isInlineable();
}
}
}
42 changes: 38 additions & 4 deletions src/main/java/org/dcsa/core/query/QueryFactoryBuilder.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,57 @@
package org.dcsa.core.query;

import org.dcsa.core.extendedrequest.CursorBackedFilterCondition;
import org.apache.commons.lang3.concurrent.BackgroundInitializer;
import org.dcsa.core.query.impl.AbstractQueryFactory;
import org.dcsa.core.query.impl.QueryFactoryBuilderImpl;
import org.springframework.data.r2dbc.dialect.R2dbcDialect;
import org.springframework.data.relational.core.sql.SelectBuilder;

import java.util.Arrays;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;

public interface QueryFactoryBuilder<T> {

QueryFactoryBuilder<T> distinct();
QueryFactoryBuilder<T> limitOffset(Function<SelectBuilder.SelectFromAndJoin, SelectBuilder.SelectFromAndJoin> limitOffsetFunction);
QueryFactoryBuilder<T> order(Function<SelectBuilder.SelectOrdered, SelectBuilder.BuildSelect> orderFunction);
QueryFactoryBuilder<T> order(BiFunction<SelectBuilder.SelectOrdered, DBEntityAnalysis<T>, SelectBuilder.BuildSelect> orderFunction);
QueryFactoryBuilder<T> r2dbcDialect(R2dbcDialect r2dbcDialect);
QueryFactoryBuilder<T> copyBuilder();
AbstractQueryFactory<T> build(CursorBackedFilterCondition filterCondition);
FrozenBuilder<QueryFactoryBuilder<T>> freeze();

ConditionBuilder<T> conditions();
AbstractQueryFactory<T> build();

static <T> QueryFactoryBuilder<T> builder(DBEntityAnalysis<T> dbEntityAnalysis) {
return QueryFactoryBuilderImpl.of(dbEntityAnalysis, false, null, null, null);
return QueryFactoryBuilderImpl.of(dbEntityAnalysis, false, null, null, null, null);
}

interface ConditionBuilder<T> {

ComparisonBuilder<T> fieldByJsonName(String jsonName);
ComparisonBuilder<T> fieldByJavaField(String javaField);
QueryFactoryBuilder<T> withQuery();

FrozenBuilder<ConditionBuilder<T>> freeze();
AbstractQueryFactory<T> build();
}

interface FrozenBuilder<B> {
B copyBuilder();
}

interface ComparisonBuilder<T> {
ConditionBuilder<T> isNull();
ConditionBuilder<T> isNotNull();
ConditionBuilder<T> greaterThan(Object value);
ConditionBuilder<T> greaterThanOrEqualTo(Object value);
ConditionBuilder<T> equalTo(Object value);
ConditionBuilder<T> lessThanOrEqualTo(Object value);
ConditionBuilder<T> lessThan(Object value);
default ConditionBuilder<T> oneOf(Object ... values) {
return oneOf(Arrays.asList(values));
}
ConditionBuilder<T> oneOf(List<Object> values);
}
}
Loading

0 comments on commit 93d601e

Please sign in to comment.