DATACOUCH-515 implement additional operators in N1qlQueryCreator and QueryCriteria (#213)
Co-authored-by: mikereiche <michael.reiche@couchbase.com>
This commit is contained in:
@@ -3,6 +3,8 @@ package org.springframework.data.couchbase.core.query;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Formatter;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class QueryCriteria implements QueryCriteriaDefinition {
|
||||
@@ -10,7 +12,7 @@ public class QueryCriteria implements QueryCriteriaDefinition {
|
||||
/**
|
||||
* Holds the chain itself, the current operator being always the last one.
|
||||
*/
|
||||
private final List<QueryCriteria> criteriaChain;
|
||||
private List<QueryCriteria> criteriaChain;
|
||||
|
||||
/**
|
||||
* Represents how the chain is hung together, null only for the first element.
|
||||
@@ -19,14 +21,25 @@ public class QueryCriteria implements QueryCriteriaDefinition {
|
||||
|
||||
private final String key;
|
||||
private String operator;
|
||||
private Object value;
|
||||
private Object[] value;
|
||||
private String format;
|
||||
|
||||
QueryCriteria(List<QueryCriteria> chain, String key, Object[] value, ChainOperator chainOperator) {
|
||||
this(chain, key, value, chainOperator, null, null);
|
||||
}
|
||||
|
||||
QueryCriteria(List<QueryCriteria> chain, String key, Object value, ChainOperator chainOperator) {
|
||||
this(chain, key, new Object[]{value}, chainOperator, null, null);
|
||||
}
|
||||
|
||||
QueryCriteria(List<QueryCriteria> chain, String key, Object[] value, ChainOperator chainOperator, String operator, String format) {
|
||||
this.criteriaChain = chain;
|
||||
criteriaChain.add(this);
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
this.chainOperator = chainOperator;
|
||||
this.operator = operator;
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,60 +65,220 @@ public class QueryCriteria implements QueryCriteriaDefinition {
|
||||
return new QueryCriteria(this.criteriaChain, key, null, ChainOperator.OR);
|
||||
}
|
||||
|
||||
private static QueryCriteria wrap(QueryCriteria criteria) {
|
||||
QueryCriteria qc = new QueryCriteria(
|
||||
new LinkedList<QueryCriteria>(),
|
||||
criteria.key,
|
||||
criteria.value,
|
||||
null,
|
||||
criteria.operator,
|
||||
criteria.format);
|
||||
return qc;
|
||||
}
|
||||
|
||||
public QueryCriteria eq(@Nullable Object o) {
|
||||
return is(o);
|
||||
}
|
||||
|
||||
public QueryCriteria is(@Nullable Object o) {
|
||||
operator = "=";
|
||||
value = o;
|
||||
value = new Object[]{o};
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryCriteria ne(@Nullable Object o) {
|
||||
operator = "!=";
|
||||
value = o;
|
||||
value = new Object[]{o};
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryCriteria lt(@Nullable Object o) {
|
||||
operator = "<";
|
||||
value = o;
|
||||
value = new Object[]{o};
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryCriteria lte(@Nullable Object o) {
|
||||
operator = "<=";
|
||||
value = o;
|
||||
value = new Object[]{o};
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryCriteria gt(@Nullable Object o) {
|
||||
operator = ">";
|
||||
value = o;
|
||||
value = new Object[]{o};
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryCriteria gte(@Nullable Object o) {
|
||||
operator = ">=";
|
||||
value = o;
|
||||
value = new Object[]{o};
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryCriteria startingWith(@Nullable Object o) {
|
||||
if (o instanceof QueryCriteria) {
|
||||
QueryCriteria term = where("\"%\"").plus(o);
|
||||
like(term);
|
||||
} else {
|
||||
like(o.toString() + "%");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryCriteria plus(@Nullable Object o) {
|
||||
operator = "PLUS";
|
||||
value = new Object[]{o};
|
||||
format = "(%1$s + %3$s)";
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryCriteria endingWith(@Nullable Object o) {
|
||||
if (o instanceof QueryCriteria) {
|
||||
QueryCriteria term = where("%").plus(o);
|
||||
like(term);
|
||||
} else {
|
||||
like("%" + o.toString());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryCriteria regex(@Nullable Object o) {
|
||||
operator = "REGEXP_LIKE";
|
||||
value = new Object[]{o};
|
||||
format = "regexp_like(%1$s, %3$s)";
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryCriteria containing(@Nullable Object o) {
|
||||
operator = "CONTAINS";
|
||||
value = new Object[]{o};
|
||||
format = "contains(%1$s, %3$s)";
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryCriteria notContaining(@Nullable Object o) {
|
||||
value = new QueryCriteria[]{wrap(containing(o))};
|
||||
operator = "NOT";
|
||||
format = format = "not( %3$s )";
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryCriteria like(@Nullable Object o) {
|
||||
operator = "LIKE";
|
||||
value = new Object[]{o};
|
||||
format = "%1$s like %3$s";
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryCriteria notLike(@Nullable Object o) {
|
||||
value = new QueryCriteria[]{wrap(like(o))};
|
||||
operator = "NOT";
|
||||
format = format = "not( %3$s )";
|
||||
return (QueryCriteria) this;
|
||||
}
|
||||
|
||||
public QueryCriteria isNull() {
|
||||
operator = "IS_NULL";
|
||||
value = null;
|
||||
format = "%1$s is null";
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryCriteria isNotNull() {
|
||||
operator = "IS_NOT_NULL";
|
||||
value = null;
|
||||
format = "%1$s is not null";
|
||||
return (QueryCriteria) this;
|
||||
}
|
||||
|
||||
public QueryCriteria isMissing() {
|
||||
operator = "IS_MISSING";
|
||||
value = null;
|
||||
format = "%1$s is missing";
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryCriteria isNotMissing() {
|
||||
operator = "IS_NOT_MiSSING";
|
||||
value = null;
|
||||
format = "%1$s is not missing";
|
||||
return (QueryCriteria) this;
|
||||
}
|
||||
|
||||
public QueryCriteria isValued() {
|
||||
operator = "IS_VALUED";
|
||||
value = null;
|
||||
format = "%1$s is valued";
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryCriteria isNotValued() {
|
||||
operator = "IS_NOT_VALUED";
|
||||
value = null;
|
||||
format = "%1$s is not valued";
|
||||
return (QueryCriteria) this;
|
||||
}
|
||||
|
||||
public QueryCriteria within(@Nullable Object o) {
|
||||
operator = "WITHIN";
|
||||
value = new Object[]{o};
|
||||
format = "%1$s within $3$s";
|
||||
return (QueryCriteria) this;
|
||||
}
|
||||
|
||||
public QueryCriteria between(@Nullable Object o1, @Nullable Object o2) {
|
||||
operator = "BETWEEN";
|
||||
value = new Object[]{o1, o2};
|
||||
format = "%1$s between %3$s and %4$s";
|
||||
return (QueryCriteria) this;
|
||||
}
|
||||
|
||||
public QueryCriteria in(@Nullable Object... o) {
|
||||
operator = "IN";
|
||||
value = o;
|
||||
StringBuilder sb = new StringBuilder("%1$s in ( [ ");
|
||||
for (int i = 1; i <= value.length; i++) { // format indices start at 1
|
||||
if (i > 1) sb.append(", ");
|
||||
sb.append("%" + (i + 2) + "$s"); // the first is fieldName, second is operator, args start at 3
|
||||
}
|
||||
format = sb.append(" ] )").toString();
|
||||
return (QueryCriteria) this;
|
||||
}
|
||||
|
||||
public QueryCriteria notIn(@Nullable Object... o) {
|
||||
value = new QueryCriteria[]{wrap(in(o))};
|
||||
operator = "NOT";
|
||||
format = format = "not( %3$s )"; // field = 1$, operator = 2$, value=$3, $4, ...
|
||||
return (QueryCriteria) this;
|
||||
}
|
||||
|
||||
public QueryCriteria TRUE() { // true/false are reserved, use TRUE/FALSE
|
||||
value = null;
|
||||
operator = null;
|
||||
format = format = "%1$s"; // field = 1$, operator = 2$, value=$3, $4, ...
|
||||
return (QueryCriteria) this;
|
||||
}
|
||||
|
||||
public QueryCriteria FALSE() {
|
||||
value = new QueryCriteria[]{wrap(TRUE())};
|
||||
operator = "not";
|
||||
format = format = "not( %3$s )";
|
||||
return (QueryCriteria) this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String export() {
|
||||
StringBuilder output = new StringBuilder();
|
||||
|
||||
boolean first = true;
|
||||
for (QueryCriteria c : this.criteriaChain) {
|
||||
if (!first) {
|
||||
if (c.chainOperator == null) {
|
||||
throw new IllegalStateException("A chain operator must be present when chaining!");
|
||||
}
|
||||
|
||||
// the consistent place to output this would be in the c.toQueryString(output) about five lines down
|
||||
output.append(" ").append(c.chainOperator.representation).append(" ");
|
||||
}
|
||||
if (first) {
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
c.exportSingle(output);
|
||||
@@ -114,15 +287,32 @@ public class QueryCriteria implements QueryCriteriaDefinition {
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
protected void exportSingle(final StringBuilder sb) {
|
||||
if (value instanceof QueryCriteria) {
|
||||
sb.append("(").append(((QueryCriteria) value).export()).append(")");
|
||||
} else {
|
||||
String fieldName = "`" + key + "`";
|
||||
sb.append(fieldName).append(" ").append(operator).append(" ").append(maybeWrapValue(value));
|
||||
private StringBuilder exportSingle(StringBuilder sb) {
|
||||
String fieldName = maybeQuote(key);
|
||||
int valueLen = value == null ? 0 : value.length;
|
||||
Object[] v = new Object[valueLen + 2];
|
||||
v[0] = fieldName;
|
||||
v[1] = operator;
|
||||
for (int i = 0; i < valueLen; i++) {
|
||||
if (value[i] instanceof QueryCriteria) {
|
||||
v[i + 2] = "(" + ((QueryCriteria) value[i]).export() + ")";
|
||||
} else {
|
||||
v[i + 2] = maybeWrapValue(value[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (key == null) { // chaining, the chainingOperator was already output by export()
|
||||
sb.append(v[2]);
|
||||
} else if (format == null) { // this always has to be fieldname <op> <something>
|
||||
sb.append(fieldName).append(" ").append(operator).append(" ").append(v[2]);
|
||||
} else {
|
||||
sb.append(new Formatter().format(format, v));
|
||||
}
|
||||
|
||||
return sb;
|
||||
}
|
||||
|
||||
|
||||
private String maybeWrapValue(Object value) {
|
||||
if (value instanceof String) {
|
||||
return "\"" + value + "\"";
|
||||
@@ -133,10 +323,17 @@ public class QueryCriteria implements QueryCriteriaDefinition {
|
||||
}
|
||||
}
|
||||
|
||||
private String maybeQuote(String value) {
|
||||
if (value == null || (value.startsWith("\"") && value.endsWith("\""))) {
|
||||
return value;
|
||||
} else {
|
||||
return "`" + value + "`";
|
||||
}
|
||||
}
|
||||
|
||||
enum ChainOperator {
|
||||
AND("and"),
|
||||
OR("or"),
|
||||
NOT("not");
|
||||
OR("or");
|
||||
|
||||
private final String representation;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ public class N1qlQueryCreator extends AbstractQueryCreator<Query, QueryCriteria>
|
||||
private final MappingContext<?, CouchbasePersistentProperty> context;
|
||||
|
||||
public N1qlQueryCreator(final PartTree tree, final ParameterAccessor accessor,
|
||||
final MappingContext<?, CouchbasePersistentProperty> context) {
|
||||
final MappingContext<?, CouchbasePersistentProperty> context) {
|
||||
super(tree, accessor);
|
||||
this.accessor = accessor;
|
||||
this.context = context;
|
||||
@@ -57,21 +57,64 @@ public class N1qlQueryCreator extends AbstractQueryCreator<Query, QueryCriteria>
|
||||
}
|
||||
|
||||
private QueryCriteria from(final Part part, final CouchbasePersistentProperty property, final QueryCriteria criteria,
|
||||
final Iterator<Object> parameters) {
|
||||
final Iterator<Object> parameters) {
|
||||
|
||||
final Part.Type type = part.getType();
|
||||
|
||||
/*
|
||||
NEAR(new String[]{"IsNear", "Near"}),
|
||||
WITHIN(new String[]{"IsWithin", "Within"}),
|
||||
*/
|
||||
switch (type) {
|
||||
case GREATER_THAN:
|
||||
case AFTER:
|
||||
return criteria.gt(parameters.next());
|
||||
case GREATER_THAN_EQUAL:
|
||||
return criteria.gte(parameters.next());
|
||||
case LESS_THAN:
|
||||
case BEFORE:
|
||||
return criteria.lt(parameters.next());
|
||||
case LESS_THAN_EQUAL:
|
||||
return criteria.lte(parameters.next());
|
||||
case SIMPLE_PROPERTY:
|
||||
return criteria.eq(parameters.next());
|
||||
case NEGATING_SIMPLE_PROPERTY:
|
||||
return criteria.ne(parameters.next());
|
||||
case CONTAINING:
|
||||
return criteria.containing(parameters.next());
|
||||
case NOT_CONTAINING:
|
||||
return criteria.notContaining(parameters.next());
|
||||
case STARTING_WITH:
|
||||
return criteria.startingWith(parameters.next());
|
||||
case ENDING_WITH:
|
||||
return criteria.endingWith(parameters.next());
|
||||
case LIKE:
|
||||
return criteria.like(parameters.next());
|
||||
case NOT_LIKE:
|
||||
return criteria.notLike(parameters.next());
|
||||
case WITHIN:
|
||||
return criteria.within(parameters.next());
|
||||
case IS_NULL:
|
||||
return criteria.isNull(/*parameters.next()*/);
|
||||
case IS_NOT_NULL:
|
||||
return criteria.isNotNull(/*parameters.next()*/);
|
||||
case IS_EMPTY:
|
||||
return criteria.isNotValued(/*parameters.next()*/);
|
||||
case IS_NOT_EMPTY:
|
||||
return criteria.isValued(/*parameters.next()*/);
|
||||
case EXISTS:
|
||||
return criteria.isNotMissing(/*parameters.next()*/);
|
||||
case REGEX:
|
||||
return criteria.regex(parameters.next());
|
||||
case BETWEEN:
|
||||
return criteria.between(parameters.next(), parameters.next());
|
||||
case IN:
|
||||
return criteria.in((Object[]) parameters.next());
|
||||
case NOT_IN:
|
||||
return criteria.notIn((Object[]) parameters.next());
|
||||
case TRUE:
|
||||
return criteria.TRUE();
|
||||
case FALSE:
|
||||
return criteria.FALSE();
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported keyword!");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user