DATACOUCH-603 Do not cast query parameters in N1qlQueryCreator.

For IN and NOT_IN - they can take varargs, an array or a JsonArray
Don't cast query criteria parameters, let parameter accessor handle that.
Fixed conversion of query criteria values to parameters
Cleaned up QueryCriteria

Co-authored-by: mikereiche <michael.reiche@couchbase.com>
This commit is contained in:
Michael Reiche
2020-10-09 14:59:16 -07:00
committed by mikereiche
parent 87397023e8
commit e4068b9a4e
10 changed files with 355 additions and 179 deletions

View File

@@ -26,6 +26,7 @@ import com.couchbase.client.java.json.JsonValue;
import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryOptions;
import com.couchbase.client.java.query.QueryScanConsistency; import com.couchbase.client.java.query.QueryScanConsistency;
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.repository.query.StringBasedN1qlQueryParser; import org.springframework.data.couchbase.repository.query.StringBasedN1qlQueryParser;
import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation;
@@ -189,7 +190,7 @@ public class Query {
sb.deleteCharAt(sb.length() - 1); sb.deleteCharAt(sb.length() - 1);
} }
public void appendWhere(final StringBuilder sb, int[] paramIndexPtr) { public void appendWhere(final StringBuilder sb, int[] paramIndexPtr, CouchbaseConverter converter) {
if (!criteria.isEmpty()) { if (!criteria.isEmpty()) {
appendWhereOrAnd(sb); appendWhereOrAnd(sb);
boolean first = true; boolean first = true;
@@ -199,16 +200,11 @@ public class Query {
} else { } else {
sb.append(" AND "); sb.append(" AND ");
} }
sb.append(c.export(paramIndexPtr)); sb.append(c.export(paramIndexPtr, parameters, converter));
} }
} }
} }
public void appendCriteria(StringBuilder sb, QueryCriteria criteria) {
appendWhereOrAnd(sb);
sb.append(criteria.export());
}
public void appendWhereString(StringBuilder sb, String whereString) { public void appendWhereString(StringBuilder sb, String whereString) {
appendWhereOrAnd(sb); appendWhereOrAnd(sb);
sb.append(whereString); sb.append(whereString);
@@ -257,9 +253,9 @@ public class Query {
return true; // is not quoted return true; // is not quoted
} }
public String export() { public String export(int[]... paramIndexPtrHolder) { // used only by tests
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
appendWhere(sb, null); appendWhere(sb, paramIndexPtrHolder.length > 0 ? paramIndexPtrHolder[0] : null, null);
appendSort(sb); appendSort(sb);
appendSkipAndLimit(sb); appendSkipAndLimit(sb);
return sb.toString(); return sb.toString();
@@ -270,7 +266,7 @@ public class Query {
final StringBuilder statement = new StringBuilder(); final StringBuilder statement = new StringBuilder();
appendString(statement, n1ql.selectEntity); // select ... appendString(statement, n1ql.selectEntity); // select ...
appendWhereString(statement, n1ql.filter); // typeKey = typeValue appendWhereString(statement, n1ql.filter); // typeKey = typeValue
appendWhere(statement, new int[] { 0 }); // criteria on this Query appendWhere(statement, new int[] { 0 }, template.getConverter()); // criteria on this Query
appendSort(statement); appendSort(statement);
appendSkipAndLimit(statement); appendSkipAndLimit(statement);
return statement.toString(); return statement.toString();
@@ -281,7 +277,7 @@ public class Query {
final StringBuilder statement = new StringBuilder(); final StringBuilder statement = new StringBuilder();
appendString(statement, n1ql.delete); // delete ... appendString(statement, n1ql.delete); // delete ...
appendWhereString(statement, n1ql.filter); // typeKey = typeValue appendWhereString(statement, n1ql.filter); // typeKey = typeValue
appendWhere(statement, null); // criteria on this Query appendWhere(statement, null, template.getConverter()); // criteria on this Query
appendString(statement, n1ql.returning); appendString(statement, n1ql.returning);
return statement.toString(); return statement.toString();
} }

View File

@@ -20,6 +20,11 @@ import java.util.Formatter;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import com.couchbase.client.core.error.InvalidArgumentException;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.json.JsonObject;
import com.couchbase.client.java.json.JsonValue;
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
/** /**
@@ -60,6 +65,10 @@ public class QueryCriteria implements QueryCriteriaDefinition {
this.format = format; this.format = format;
} }
Object[] getValue() {
return value;
}
/** /**
* Static factory method to create a Criteria using the provided key. * Static factory method to create a Criteria using the provided key.
*/ */
@@ -68,8 +77,8 @@ public class QueryCriteria implements QueryCriteriaDefinition {
} }
private static QueryCriteria wrap(QueryCriteria criteria) { private static QueryCriteria wrap(QueryCriteria criteria) {
QueryCriteria qc = new QueryCriteria(new LinkedList<QueryCriteria>(), criteria.key, criteria.value, null, QueryCriteria qc = new QueryCriteria(new LinkedList<>(), criteria.key, criteria.value, null, criteria.operator,
criteria.operator, criteria.format); criteria.format);
return qc; return qc;
} }
@@ -167,7 +176,7 @@ public class QueryCriteria implements QueryCriteriaDefinition {
public QueryCriteria notContaining(@Nullable Object o) { public QueryCriteria notContaining(@Nullable Object o) {
value = new QueryCriteria[] { wrap(containing(o)) }; value = new QueryCriteria[] { wrap(containing(o)) };
operator = "NOT"; operator = "NOT";
format = format = "not( %3$s )"; format = "not( %3$s )";
return this; return this;
} }
@@ -196,7 +205,7 @@ public class QueryCriteria implements QueryCriteriaDefinition {
operator = "IS_NOT_NULL"; operator = "IS_NOT_NULL";
value = null; value = null;
format = "%1$s is not null"; format = "%1$s is not null";
return (QueryCriteria) this; return this;
} }
public QueryCriteria isMissing() { public QueryCriteria isMissing() {
@@ -210,7 +219,7 @@ public class QueryCriteria implements QueryCriteriaDefinition {
operator = "IS_NOT_MiSSING"; operator = "IS_NOT_MiSSING";
value = null; value = null;
format = "%1$s is not missing"; format = "%1$s is not missing";
return (QueryCriteria) this; return this;
} }
public QueryCriteria isValued() { public QueryCriteria isValued() {
@@ -224,68 +233,74 @@ public class QueryCriteria implements QueryCriteriaDefinition {
operator = "IS_NOT_VALUED"; operator = "IS_NOT_VALUED";
value = null; value = null;
format = "%1$s is not valued"; format = "%1$s is not valued";
return (QueryCriteria) this; return this;
} }
public QueryCriteria within(@Nullable Object o) { public QueryCriteria within(@Nullable Object o) {
operator = "WITHIN"; operator = "WITHIN";
value = new Object[] { o }; value = new Object[] { o };
format = "%1$s within $3$s"; format = "%1$s within %3$s";
return (QueryCriteria) this; return this;
} }
public QueryCriteria between(@Nullable Object o1, @Nullable Object o2) { public QueryCriteria between(@Nullable Object o1, @Nullable Object o2) {
operator = "BETWEEN"; operator = "BETWEEN";
value = new Object[] { o1, o2 }; value = new Object[] { o1, o2 };
format = "%1$s between %3$s and %4$s"; format = "%1$s between %3$s and %4$s";
return (QueryCriteria) this; return this;
} }
public QueryCriteria in(@Nullable Object... o) { public QueryCriteria in(@Nullable Object... o) {
operator = "IN"; operator = "IN";
value = o; format = "%1$s in ( %3$s )";
StringBuilder sb = new StringBuilder("%1$s in ( [ "); // IN takes a single argument that is a list
for (int i = 1; i <= value.length; i++) { // format indices start at 1 if (o.length > 0) {
if (i > 1) if (o[0] instanceof JsonArray || o[0] instanceof List || o[0] instanceof Object[]) {
sb.append(", "); if (o.length != 1) {
sb.append("%" + (i + 2) + "$s"); // the first is fieldName, second is operator, args start at 3 throw new RuntimeException("IN cannot take multiple lists");
}
value = o;
} else {
value = new Object[1];
value[0] = o; // JsonArray.from(o);
}
} }
format = sb.append(" ] )").toString(); return this;
return (QueryCriteria) this;
} }
public QueryCriteria notIn(@Nullable Object... o) { public QueryCriteria notIn(@Nullable Object... o) {
value = new QueryCriteria[] { wrap(in(o)) }; value = new QueryCriteria[] { wrap(in(o)) };
operator = "NOT"; operator = "NOT";
format = format = "not( %3$s )"; // field = 1$, operator = 2$, value=$3, $4, ... format = "not( %3$s )"; // field = 1$, operator = 2$, value=$3, $4, ...
return (QueryCriteria) this; return this;
} }
public QueryCriteria TRUE() { // true/false are reserved, use TRUE/FALSE public QueryCriteria TRUE() { // true/false are reserved, use TRUE/FALSE
value = null; value = null;
operator = null; operator = null;
format = format = "%1$s"; // field = 1$, operator = 2$, value=$3, $4, ... format = "%1$s"; // field = 1$, operator = 2$, value=$3, $4, ...
return (QueryCriteria) this; return this;
} }
public QueryCriteria FALSE() { public QueryCriteria FALSE() {
value = new QueryCriteria[] { wrap(TRUE()) }; value = new QueryCriteria[] { wrap(TRUE()) };
operator = "not"; operator = "not";
format = format = "not( %3$s )"; format = "not( %3$s )";
return (QueryCriteria) this; return this;
} }
/** /**
* This exports the query criteria into a string to be appended to the beginning of an N1QL statement * This exports the query criteria chain into a string to be appended to the beginning of an N1QL statement
* *
* @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters * @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters There may
* There may already be positional parameters in the beginning of the statement, * already be positional parameters in the beginning of the statement, so it may not always start at 1. If it
* so it may not always start at 1. If it has the value -1, the query is using * has the value -1, the query is using named parameters. If the pointer is null, the query is not using
* named parameters. If the pointer is null, the query is not using parameters. * parameters.
* @param parameters - parameters of the query. If operands are parameterized, their values are added to parameters
* @return string containing part of N1QL query * @return string containing part of N1QL query
*/ */
@Override @Override
public String export(int[] paramIndexPtr) { public String export(int[] paramIndexPtr, JsonValue parameters, CouchbaseConverter converter) {
StringBuilder output = new StringBuilder(); StringBuilder output = new StringBuilder();
boolean first = true; boolean first = true;
for (QueryCriteria c : this.criteriaChain) { for (QueryCriteria c : this.criteriaChain) {
@@ -298,7 +313,7 @@ public class QueryCriteria implements QueryCriteriaDefinition {
} else { } else {
first = false; first = false;
} }
c.exportSingle(output, paramIndexPtr); c.exportSingle(output, paramIndexPtr, parameters, converter);
} }
return output.toString(); return output.toString();
@@ -310,12 +325,24 @@ public class QueryCriteria implements QueryCriteriaDefinition {
* @return string containing part of N1QL query * @return string containing part of N1QL query
*/ */
@Override @Override
public String export() { public String export() { // used only by tests
return export(null); return export(null, null, null);
} }
private StringBuilder exportSingle(StringBuilder sb, int[] paramIndexPtr) { /**
* Appends the query criteria to a StringBuilder which will be appended to a N1QL statement
*
* @param sb - the string builder
* @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters There may
* already be positional parameters in the beginning of the statement, so it may not always start at 1. If it
* has the value -1, the query is using named parameters. If the pointer is null, the query is not using
* parameters.
* @param parameters - parameters of the query. If operands are parameterized, their values are added to parameters
* @return string containing part of N1QL query
*/
private StringBuilder exportSingle(StringBuilder sb, int[] paramIndexPtr, JsonValue parameters,
CouchbaseConverter converter) {
String fieldName = maybeQuote(key); String fieldName = maybeQuote(key);
int valueLen = value == null ? 0 : value.length; int valueLen = value == null ? 0 : value.length;
Object[] v = new Object[valueLen + 2]; Object[] v = new Object[valueLen + 2];
@@ -323,9 +350,9 @@ public class QueryCriteria implements QueryCriteriaDefinition {
v[1] = operator; v[1] = operator;
for (int i = 0; i < valueLen; i++) { for (int i = 0; i < valueLen; i++) {
if (value[i] instanceof QueryCriteria) { if (value[i] instanceof QueryCriteria) {
v[i + 2] = "(" + ((QueryCriteria) value[i]).export(paramIndexPtr) + ")"; v[i + 2] = "(" + ((QueryCriteria) value[i]).export(paramIndexPtr, parameters, converter) + ")";
} else { } else {
v[i + 2] = maybeWrapValue(key, value[i], paramIndexPtr); v[i + 2] = maybeWrapValue(key, value[i], paramIndexPtr, parameters, converter);
} }
} }
@@ -340,24 +367,84 @@ public class QueryCriteria implements QueryCriteriaDefinition {
return sb; return sb;
} }
private String maybeWrapValue(String key, Object value, int[] paramIndexPtr) { /**
* Possibly convert an operand to a positional or named parameter
*
* @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters There may
* already be positional parameters in the beginning of the statement, so it may not always start at 1. If it
* has the value -1, the query is using named parameters. If the pointer is null, the query is not using
* parameters.
* @param parameters - parameters of the query. If operands are parameterized, their values are added to parameters
* @return string containing part of N1QL query
*/
private String maybeWrapValue(String key, Object value, int[] paramIndexPtr, JsonValue parameters,
CouchbaseConverter converter) {
if (paramIndexPtr != null) { if (paramIndexPtr != null) {
if (paramIndexPtr[0] >= 0) { if (paramIndexPtr[0] >= 0) {
JsonArray params = (JsonArray) parameters;
// from StringBasedN1qlQueryParser.getPositionalPlaceholderValues()
try {
params.add(convert(converter, value));
} catch (InvalidArgumentException iae) {
if (value instanceof Object[]) {
addAsArray(params, value, converter);
} else {
throw iae;
}
}
return "$" + (++paramIndexPtr[0]); // these are generated in order return "$" + (++paramIndexPtr[0]); // these are generated in order
} else { } else {
JsonObject params = (JsonObject) parameters;
// from StringBasedN1qlQueryParser.getNamedPlaceholderValues()
try {
params.put(key, convert(converter, value));
} catch (InvalidArgumentException iae) {
if (value instanceof Object[]) {
params.put(key, JsonArray.from((Object[]) value));
} else {
throw iae;
}
}
return "$" + key; return "$" + key;
} }
} }
// Did not convert to a parameter. Add quotes or whatever it might need.
if (value instanceof String) { if (value instanceof String) {
return "\"" + value + "\""; return "\"" + value + "\"";
} else if (value == null) { } else if (value == null) {
return "null"; return "null";
} else if (value instanceof Object[]) { // convert array into sequence of comma-separated values
StringBuffer l = new StringBuffer();
l.append("[");
Object[] array = (Object[]) value;
for (int i = 0; i < array.length; i++) {
if (i > 0) {
l.append(",");
}
l.append(maybeWrapValue(null, array[i], null, null, converter));
}
l.append("]");
return l.toString();
} else { } else {
return value.toString(); return value.toString();
} }
} }
private static Object convert(CouchbaseConverter converter, Object value) {
return converter != null ? converter.convertForWriteIfNeeded(value) : value;
}
private void addAsArray(JsonArray posValues, Object o, CouchbaseConverter converter) {
Object[] array = (Object[]) o;
JsonArray ja = JsonValue.ja();
for (Object e : array) {
ja.add(String.valueOf(convert(converter, e)));
}
posValues.add(ja);
}
private String maybeQuote(String value) { private String maybeQuote(String value) {
if (value == null || (value.startsWith("\"") && value.endsWith("\""))) { if (value == null || (value.startsWith("\"") && value.endsWith("\""))) {
return value; return value;

View File

@@ -15,6 +15,9 @@
*/ */
package org.springframework.data.couchbase.core.query; package org.springframework.data.couchbase.core.query;
import com.couchbase.client.java.json.JsonValue;
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
/** /**
* @author Oliver Gierke * @author Oliver Gierke
* @author Christoph Strobl * @author Christoph Strobl
@@ -25,13 +28,15 @@ public interface QueryCriteriaDefinition {
/** /**
* This exports the query criteria into a string to be appended to the beginning of an N1QL statement * This exports the query criteria into a string to be appended to the beginning of an N1QL statement
* *
* @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters * @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters There may
* There may already be positional parameters in the beginning of the statement, * already be positional parameters in the beginning of the statement, so it may not always start at 1. If it
* so it may not always start at 1. If it has the value -1, the query is using * has the value -1, the query is using named parameters. If the pointer is null, the query is not using
* named parameters. If the pointer is null, the query is not using parameters. * parameters.
* @param parameters - query parameters. Criteria values that are converted to arguments are added to parameters
* @param converter - converter to use for converting criteria values
* @return string containing part of N1QL query * @return string containing part of N1QL query
*/ */
String export(int[] paramIndexPtr); String export(int[] paramIndexPtr, JsonValue parameters, CouchbaseConverter converter);
/** /**
* Export the query criteria to a string without using positional or named parameters. * Export the query criteria to a string without using positional or named parameters.

View File

@@ -66,7 +66,8 @@ public class StringQuery extends Query {
} else { // named parameters or no parameters, no index required } else { // named parameters or no parameters, no index required
paramIndexPtr = new int[] { -1 }; paramIndexPtr = new int[] { -1 };
} }
appendWhere(statement, paramIndexPtr); // criteria on this Query - should be empty for StringQuery appendWhere(statement, paramIndexPtr, template.getConverter()); // criteria on this Query - should be empty for
// StringQuery
appendSort(statement); appendSort(statement);
appendSkipAndLimit(statement); appendSkipAndLimit(statement);
return statement.toString(); return statement.toString();

View File

@@ -17,12 +17,8 @@ package org.springframework.data.couchbase.repository.query;
import static org.springframework.data.couchbase.core.query.QueryCriteria.*; import static org.springframework.data.couchbase.core.query.QueryCriteria.*;
import java.lang.reflect.Array;
import java.util.Iterator; import java.util.Iterator;
import com.couchbase.client.core.error.InvalidArgumentException;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.json.JsonValue;
import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.query.Query;
@@ -30,7 +26,6 @@ import org.springframework.data.couchbase.core.query.QueryCriteria;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.data.repository.query.parser.AbstractQueryCreator;
@@ -59,8 +54,7 @@ public class N1qlQueryCreator extends AbstractQueryCreator<Query, QueryCriteria>
@Override @Override
protected QueryCriteria create(final Part part, final Iterator<Object> iterator) { protected QueryCriteria create(final Part part, final Iterator<Object> iterator) {
PersistentPropertyPath<CouchbasePersistentProperty> path = context.getPersistentPropertyPath( PersistentPropertyPath<CouchbasePersistentProperty> path = context.getPersistentPropertyPath(part.getProperty());
part.getProperty());
CouchbasePersistentProperty property = path.getLeafProperty(); CouchbasePersistentProperty property = path.getLeafProperty();
return from(part, property, where(path.toDotPath()), iterator); return from(part, property, where(path.toDotPath()), iterator);
} }
@@ -71,8 +65,7 @@ public class N1qlQueryCreator extends AbstractQueryCreator<Query, QueryCriteria>
return create(part, iterator); return create(part, iterator);
} }
PersistentPropertyPath<CouchbasePersistentProperty> path = context.getPersistentPropertyPath( PersistentPropertyPath<CouchbasePersistentProperty> path = context.getPersistentPropertyPath(part.getProperty());
part.getProperty());
CouchbasePersistentProperty property = path.getLeafProperty(); CouchbasePersistentProperty property = path.getLeafProperty();
return from(part, property, base.and(path.toDotPath()), iterator); return from(part, property, base.and(path.toDotPath()), iterator);
@@ -85,92 +78,70 @@ public class N1qlQueryCreator extends AbstractQueryCreator<Query, QueryCriteria>
@Override @Override
protected Query complete(QueryCriteria criteria, Sort sort) { protected Query complete(QueryCriteria criteria, Sort sort) {
JsonArray params = (JsonArray) getPositionalPlaceholderValues(accessor); return (criteria == null ? new Query() : new Query().addCriteria(criteria)).with(sort);
return (criteria == null ? new Query() : new Query().addCriteria(criteria)).with(sort).setPositionalParameters(
params);
} }
private QueryCriteria from(final Part part, final CouchbasePersistentProperty property, private QueryCriteria from(final Part part, final CouchbasePersistentProperty property, final QueryCriteria criteria,
final QueryCriteria criteria, final Iterator<Object> parameters) { final Iterator<Object> parameters) {
final Part.Type type = part.getType(); final Part.Type type = part.getType();
/* /*
NEAR(new String[]{"IsNear", "Near"}), NEAR(new String[]{"IsNear", "Near"}),
*/ */
switch (type) { switch (type) {
case GREATER_THAN: case GREATER_THAN:
case AFTER: case AFTER:
return criteria.gt(parameters.next()); return criteria.gt(parameters.next());
case GREATER_THAN_EQUAL: case GREATER_THAN_EQUAL:
return criteria.gte(parameters.next()); return criteria.gte(parameters.next());
case LESS_THAN: case LESS_THAN:
case BEFORE: case BEFORE:
return criteria.lt(parameters.next()); return criteria.lt(parameters.next());
case LESS_THAN_EQUAL: case LESS_THAN_EQUAL:
return criteria.lte(parameters.next()); return criteria.lte(parameters.next());
case SIMPLE_PROPERTY: case SIMPLE_PROPERTY:
return criteria.eq(parameters.next()); return criteria.eq(parameters.next());
case NEGATING_SIMPLE_PROPERTY: case NEGATING_SIMPLE_PROPERTY:
return criteria.ne(parameters.next()); return criteria.ne(parameters.next());
case CONTAINING: case CONTAINING:
return criteria.containing(parameters.next()); return criteria.containing(parameters.next());
case NOT_CONTAINING: case NOT_CONTAINING:
return criteria.notContaining(parameters.next()); return criteria.notContaining(parameters.next());
case STARTING_WITH: case STARTING_WITH:
return criteria.startingWith(parameters.next()); return criteria.startingWith(parameters.next());
case ENDING_WITH: case ENDING_WITH:
return criteria.endingWith(parameters.next()); return criteria.endingWith(parameters.next());
case LIKE: case LIKE:
return criteria.like(parameters.next()); return criteria.like(parameters.next());
case NOT_LIKE: case NOT_LIKE:
return criteria.notLike(parameters.next()); return criteria.notLike(parameters.next());
case WITHIN: case WITHIN:
return criteria.within(parameters.next()); return criteria.within(parameters.next());
case IS_NULL: case IS_NULL:
return criteria.isNull(/*parameters.next()*/); return criteria.isNull(/*parameters.next()*/);
case IS_NOT_NULL: case IS_NOT_NULL:
return criteria.isNotNull(/*parameters.next()*/); return criteria.isNotNull(/*parameters.next()*/);
case IS_EMPTY: case IS_EMPTY:
return criteria.isNotValued(/*parameters.next()*/); return criteria.isNotValued(/*parameters.next()*/);
case IS_NOT_EMPTY: case IS_NOT_EMPTY:
return criteria.isValued(/*parameters.next()*/); return criteria.isValued(/*parameters.next()*/);
case EXISTS: case EXISTS:
return criteria.isNotMissing(/*parameters.next()*/); return criteria.isNotMissing(/*parameters.next()*/);
case REGEX: case REGEX:
return criteria.regex(parameters.next()); return criteria.regex(parameters.next());
case BETWEEN: case BETWEEN:
return criteria.between(parameters.next(), parameters.next()); return criteria.between(parameters.next(), parameters.next());
case IN: case IN:
return criteria.in((Object[]) parameters.next()); return criteria.in(parameters.next());
case NOT_IN: case NOT_IN:
return criteria.notIn((Object[]) parameters.next()); return criteria.notIn(parameters.next());
case TRUE: case TRUE:
return criteria.TRUE(); return criteria.TRUE();
case FALSE: case FALSE:
return criteria.FALSE(); return criteria.FALSE();
default: default:
throw new IllegalArgumentException("Unsupported keyword!"); throw new IllegalArgumentException("Unsupported keyword!");
} }
} }
// from StringN1qlQueryParser
private JsonValue getPositionalPlaceholderValues(ParameterAccessor accessor) {
JsonArray posValues = JsonArray.create();
if (queryMethod == null)
return posValues;
for (Parameter parameter : this.queryMethod.getParameters().getBindableParameters()) {
try {
posValues.add(converter.convertForWriteIfNeeded(accessor.getBindableValue(parameter.getIndex())));
} catch (InvalidArgumentException iae) {
Object o = accessor.getBindableValue(parameter.getIndex());
if (o instanceof Object[]) {
Object[] array = (Object[]) o;
for (Object e : array) {
posValues.add(converter.convertForWriteIfNeeded(e));
}
}
}
}
return posValues;
}
} }

View File

@@ -189,14 +189,14 @@ public class StringBasedN1qlQueryParser {
if (checkNotQuoted(placeholder, namedMatcher.start(), namedMatcher.end(), quotes)) { if (checkNotQuoted(placeholder, namedMatcher.start(), namedMatcher.end(), quotes)) {
LOGGER.trace("{}: Found named placeholder {}", this.queryMethod.getName(), placeholder); LOGGER.trace("{}: Found named placeholder {}", this.queryMethod.getName(), placeholder);
namedCount++; namedCount++;
parameterNames.add(placeholder.substring(1));//save without the leading $ parameterNames.add(placeholder.substring(1));// save without the leading $
} }
} }
if (posCount > 0 && namedCount > 0) { // actual values from parameterNames might be more useful if (posCount > 0 && namedCount > 0) { // actual values from parameterNames might be more useful
throw new IllegalArgumentException("Using both named (" + namedCount + ") and positional (" + posCount throw new IllegalArgumentException("Using both named (" + namedCount + ") and positional (" + posCount
+ ") placeholders is not supported, please choose one over the other in " + ") placeholders is not supported, please choose one over the other in " + queryMethod.getClass().getName()
+ queryMethod.getClass().getName() + "." + this.queryMethod.getName() + "()"); + "." + this.queryMethod.getName() + "()");
} }
if (posCount > 0) { if (posCount > 0) {
@@ -211,8 +211,7 @@ public class StringBasedN1qlQueryParser {
private boolean checkNotQuoted(String item, int start, int end, List<int[]> quotes) { private boolean checkNotQuoted(String item, int start, int end, List<int[]> quotes) {
for (int[] quote : quotes) { for (int[] quote : quotes) {
if (quote[0] <= start && quote[1] >= end) { if (quote[0] <= start && quote[1] >= end) {
LOGGER.trace("{}: potential placeholder {} is inside quotes, ignored", this.queryMethod.getName(), LOGGER.trace("{}: potential placeholder {} is inside quotes, ignored", this.queryMethod.getName(), item);
item);
return false; return false;
} }
} }
@@ -220,7 +219,7 @@ public class StringBasedN1qlQueryParser {
} }
/** /**
* Get Postional argument placeholders to use for parameters. $1, $2 etc. * Get Postional argument placeholders to use for parameters. $1, $2 etc.
* *
* @param accessor * @param accessor
* @return - JsonValue holding parameters. * @return - JsonValue holding parameters.
@@ -230,7 +229,7 @@ public class StringBasedN1qlQueryParser {
for (Parameter parameter : this.queryMethod.getParameters().getBindableParameters()) { for (Parameter parameter : this.queryMethod.getParameters().getBindableParameters()) {
Object rawValue = accessor.getBindableValue(parameter.getIndex()); Object rawValue = accessor.getBindableValue(parameter.getIndex());
Object value = couchbaseConverter.convertForWriteIfNeeded(rawValue); Object value = couchbaseConverter.convertForWriteIfNeeded(rawValue);
putPositionalValue(accessor, parameter, posValues, value); putPositionalValue(posValues, value);
} }
return posValues; return posValues;
} }
@@ -251,22 +250,20 @@ public class StringBasedN1qlQueryParser {
if (placeholder != null && placeholder.charAt(0) == ':') { if (placeholder != null && placeholder.charAt(0) == ':') {
placeholder = placeholder.replaceFirst(":", ""); placeholder = placeholder.replaceFirst(":", "");
putNamedValue(accessor, parameter, namedValues, placeholder, value); putNamedValue(namedValues, placeholder, value);
if (pNames.contains(placeholder)) { if (pNames.contains(placeholder)) {
pNames.remove(placeholder); pNames.remove(placeholder);
} else { } else {
throw new RuntimeException( throw new RuntimeException("parameter named " + placeholder + " does not match any named parameter "
"parameter named " + placeholder + " does not match any named parameter " + parameterNames + parameterNames + " in " + statement);
+ " in " + statement);
} }
} else { } else {
if (parameter.getName().isPresent()) { if (parameter.getName().isPresent()) {
putNamedValue(accessor, parameter, namedValues, parameter.getName().get(), value); putNamedValue(namedValues, parameter.getName().get(), value);
} else { } else {
throw new RuntimeException( throw new RuntimeException("cannot determine argument for named parameter. " + "Argument "
"cannot determine argument for named parameter. " + "Argument " + parameter.getIndex() + parameter.getIndex() + " to " + queryMethod.getClass().getName() + "." + queryMethod.getName()
+ " to " + queryMethod.getClass().getName() + "." + queryMethod.getName() + "() needs @Param(\"name\") that matches a named parameter in " + statement);
+ "() needs @Param(\"name\") that matches a named parameter in " + statement);
} }
} }
} }
@@ -278,18 +275,17 @@ public class StringBasedN1qlQueryParser {
protected JsonValue getPlaceholderValues(ParameterAccessor accessor) { protected JsonValue getPlaceholderValues(ParameterAccessor accessor) {
switch (this.placeHolderType) { switch (this.placeHolderType) {
case NAMED: case NAMED:
return getNamedPlaceholderValues(accessor); return getNamedPlaceholderValues(accessor);
case POSITIONAL: case POSITIONAL:
return getPositionalPlaceholderValues(accessor); return getPositionalPlaceholderValues(accessor);
case NONE: case NONE:
default: default:
return JsonArray.create(); return JsonArray.create();
} }
} }
private void putPositionalValue(ParameterAccessor accessor, Parameter parameter, JsonArray posValues, private void putPositionalValue(JsonArray posValues, Object value) {
Object value) {
try { try {
posValues.add(value); posValues.add(value);
} catch (InvalidArgumentException iae) { } catch (InvalidArgumentException iae) {
@@ -310,8 +306,7 @@ public class StringBasedN1qlQueryParser {
posValues.add(ja); posValues.add(ja);
} }
private void putNamedValue(ParameterAccessor accessor, Parameter parameter, JsonObject namedValues, private void putNamedValue(JsonObject namedValues, String placeholder, Object value) {
String placeholder, Object value) {
try { try {
namedValues.put(placeholder, value); namedValues.put(placeholder, value);
} catch (InvalidArgumentException iae) { } catch (InvalidArgumentException iae) {

View File

@@ -145,7 +145,7 @@ public class StringN1qlQueryCreator extends AbstractQueryCreator<Query, QueryCri
final Part.Type type = part.getType(); final Part.Type type = part.getType();
switch (type) { switch (type) {
case SIMPLE_PROPERTY: case SIMPLE_PROPERTY:
return criteria; //.eq(parameters.next()); // this will be the dummy from PartTree return criteria; // this will be the dummy from PartTree
default: default:
throw new IllegalArgumentException("Unsupported keyword!"); throw new IllegalArgumentException("Unsupported keyword!");
} }

View File

@@ -19,7 +19,18 @@ package org.springframework.data.couchbase.core.query;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.data.couchbase.core.query.QueryCriteria.*; import static org.springframework.data.couchbase.core.query.QueryCriteria.*;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.json.JsonObject;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.data.couchbase.domain.User;
import org.springframework.data.couchbase.domain.UserRepository;
import org.springframework.data.couchbase.repository.query.N1qlQueryCreator;
import org.springframework.data.repository.query.parser.PartTree;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
class QueryCriteriaTests { class QueryCriteriaTests {
@@ -56,21 +67,25 @@ class QueryCriteriaTests {
@Test @Test
void testNestedAndCriteria() { void testNestedAndCriteria() {
QueryCriteria c = where("name").is("Bubba").and(where("age").gt(12).or("country").is("Austria")); QueryCriteria c = where("name").is("Bubba").and(where("age").gt(12).or("country").is("Austria"));
assertEquals("`name` = \"Bubba\" and (`age` > 12 or `country` = \"Austria\")", c.export()); JsonArray parameters = JsonArray.create();
assertEquals("`name` = $1 and (`age` > $2 or `country` = $3)", c.export(new int[1], parameters, null));
assertEquals("[\"Bubba\",12,\"Austria\"]", parameters.toString());
} }
@Test @Test
void testNestedOrCriteria() { void testNestedOrCriteria() {
QueryCriteria c = where("name").is("Bubba").or(where("age").gt(12).or("country").is("Austria")); QueryCriteria c = where("name").is("Bubba").or(where("age").gt(12).or("country").is("Austria"));
assertEquals("`name` = \"Bubba\" or (`age` > 12 or `country` = \"Austria\")", c.export()); JsonArray parameters = JsonArray.create();
assertEquals("`name` = $1 or (`age` > $2 or `country` = $3)", c.export(new int[1], parameters, null));
assertEquals("[\"Bubba\",12,\"Austria\"]", parameters.toString());
} }
@Test @Test
void testNestedNotIn() { void testNestedNotIn() {
QueryCriteria c = where("name").is("Bubba").or(where("age").gt(12).or("country").is("Austria")).and( QueryCriteria c = where("name").is("Bubba").or(where("age").gt(12).or("country").is("Austria"))
where("state").notIn(new String[] { "Alabama", "Florida" })); .and(where("state").notIn(new String[] { "Alabama", "Florida" }));
assertEquals("`name` = \"Bubba\" or (`age` > 12 or `country` = \"Austria\") and " assertEquals("`name` = \"Bubba\" or (`age` > 12 or `country` = \"Austria\") and "
+ "(not( (`state` in ( [ \"Alabama\", \"Florida\" ] )) ))", c.export()); + "(not( (`state` in ( [\"Alabama\",\"Florida\"] )) ))", c.export());
} }
@Test @Test
@@ -110,13 +125,13 @@ class QueryCriteriaTests {
} }
/* cannot do this properly yet because in arg to when() in /* cannot do this properly yet because in arg to when() in
* startingWith() cannot be a QueryCriteria * startingWith() cannot be a QueryCriteria
@Test @Test
void testStartingWithExpr() { void testStartingWithExpr() {
QueryCriteria c = where("name").startingWith(where("name").plus("xxx")); QueryCriteria c = where("name").startingWith(where("name").plus("xxx"));
assertEquals("`name` like (((`name` || "xxx") || ""%""))", c.export()); assertEquals("`name` like (((`name` || "xxx") || ""%""))", c.export());
} }
*/ */
@Test @Test
void testEndingWith() { void testEndingWith() {
@@ -204,14 +219,22 @@ class QueryCriteriaTests {
@Test @Test
void testIn() { void testIn() {
QueryCriteria c = where("name").in(new String[] { "gump", "davis" }); String[] args = new String[] { "gump", "davis" };
assertEquals("`name` in ( [ \"gump\", \"davis\" ] )", c.export()); QueryCriteria c = where("name").in(args);
assertEquals("`name` in ( [\"gump\",\"davis\"] )", c.export());
JsonArray parameters = JsonArray.create();
assertEquals("`name` in ( $1 )", c.export(new int[1], parameters, null));
assertEquals(arrayToString(args), parameters.get(0).toString());
} }
@Test @Test
void testNotIn() { void testNotIn() {
QueryCriteria c = where("name").notIn(new String[] { "gump", "davis" }); String[] args = new String[] { "gump", "davis" };
assertEquals("not( (`name` in ( [ \"gump\", \"davis\" ] )) )", c.export()); QueryCriteria c = where("name").notIn(args);
assertEquals("not( (`name` in ( [\"gump\",\"davis\"] )) )", c.export());
JsonArray parameters = JsonArray.create();
assertEquals("not( (`name` in ( $1 )) )", c.export(new int[1], parameters, null));
assertEquals(arrayToString(args), parameters.get(0).toString());
} }
@Test @Test
@@ -225,4 +248,28 @@ class QueryCriteriaTests {
QueryCriteria c = where("name").FALSE(); QueryCriteria c = where("name").FALSE();
assertEquals("not( (`name`) )", c.export()); assertEquals("not( (`name`) )", c.export());
} }
private String arrayToString(Object[] array) {
StringBuilder sb = new StringBuilder();
if (array != null) {
sb.append("[");
boolean first = true;
for (Object e : array) {
if (!first) {
sb.append(",");
}
first = false;
if (e instanceof Number)
sb.append(e);
else {
sb.append("\"");
sb.append(e);
sb.append("\"");
}
}
sb.append("]");
}
return sb.toString();
}
} }

View File

@@ -18,6 +18,7 @@ package org.springframework.data.couchbase.domain;
import java.util.List; import java.util.List;
import com.couchbase.client.java.json.JsonArray;
import org.springframework.data.couchbase.repository.Query; import org.springframework.data.couchbase.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
@@ -34,6 +35,10 @@ public interface UserRepository extends PagingAndSortingRepository<User, String>
List<User> findByFirstname(String firstname); List<User> findByFirstname(String firstname);
List<User> findByFirstnameIn(String... firstnames);
List<User> findByFirstnameIn(JsonArray firstnames);
List<User> findByFirstnameAndLastname(String firstname, String lastname); List<User> findByFirstnameAndLastname(String firstname, String lastname);
@Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and firstname = $1 and lastname = $2") @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and firstname = $1 and lastname = $2")

View File

@@ -19,7 +19,13 @@ import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.data.couchbase.core.query.QueryCriteria.*; import static org.springframework.data.couchbase.core.query.QueryCriteria.*;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.json.JsonObject;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
@@ -62,6 +68,69 @@ class N1qlQueryCreatorTests {
assertEquals(query.export(), " WHERE " + where("firstname").is("Oliver").export()); assertEquals(query.export(), " WHERE " + where("firstname").is("Oliver").export());
} }
@Test
void queryParametersArray() throws Exception {
String input = "findByFirstnameIn";
PartTree tree = new PartTree(input, User.class);
Method method = UserRepository.class.getMethod(input, String[].class);
Query expected = (new Query()).addCriteria(where("firstname").in("Oliver", "Charles"));
N1qlQueryCreator creator = new N1qlQueryCreator(tree,
getAccessor(getParameters(method), new Object[] { new Object[] { "Oliver", "Charles" } }), null, converter);
Query query = creator.createQuery();
// Query expected = (new Query()).addCriteria(where("firstname").in("Oliver", "Charles"));
assertEquals(expected.export(new int[1]), query.export(new int[1]));
JsonObject expectedOptions = JsonObject.create();
expected.buildQueryOptions(null).build().injectParams(expectedOptions);
JsonObject actualOptions = JsonObject.create();
expected.buildQueryOptions(null).build().injectParams(actualOptions);
assertEquals(expectedOptions.removeKey("client_context_id"), actualOptions.removeKey("client_context_id"));
}
@Test
void queryParametersJsonArray() throws Exception {
String input = "findByFirstnameIn";
PartTree tree = new PartTree(input, User.class);
Method method = UserRepository.class.getMethod(input, JsonArray.class);
JsonArray jsonArray = JsonArray.create();
jsonArray.add("Oliver");
jsonArray.add("Charles");
N1qlQueryCreator creator = new N1qlQueryCreator(tree, getAccessor(getParameters(method), jsonArray), null,
converter);
Query query = creator.createQuery();
Query expected = (new Query()).addCriteria(where("firstname").in("Oliver", "Charles"));
assertEquals(expected.export(new int[1]), query.export(new int[1]));
JsonObject expectedOptions = JsonObject.create();
expected.buildQueryOptions(null).build().injectParams(expectedOptions);
JsonObject actualOptions = JsonObject.create();
expected.buildQueryOptions(null).build().injectParams(actualOptions);
assertEquals(expectedOptions.removeKey("client_context_id"), actualOptions.removeKey("client_context_id"));
}
@Test
void queryParametersList() throws Exception {
String input = "findByFirstnameIn";
PartTree tree = new PartTree(input, User.class);
Method method = UserRepository.class.getMethod(input, String[].class);
List<String> list = new LinkedList<>();
list.add("Oliver");
list.add("Charles");
N1qlQueryCreator creator = new N1qlQueryCreator(tree, getAccessor(getParameters(method), new Object[] { list }),
null, converter);
Query query = creator.createQuery();
Query expected = (new Query()).addCriteria(where("firstname").in("Oliver", "Charles"));
assertEquals(expected.export(new int[1]), query.export(new int[1]));
JsonObject expectedOptions = JsonObject.create();
expected.buildQueryOptions(null).build().injectParams(expectedOptions);
JsonObject actualOptions = JsonObject.create();
expected.buildQueryOptions(null).build().injectParams(actualOptions);
assertEquals(expectedOptions.removeKey("client_context_id"), actualOptions.removeKey("client_context_id"));
}
@Test @Test
void createsAndQueryCorrectly() throws Exception { void createsAndQueryCorrectly() throws Exception {
String input = "findByFirstnameAndLastname"; String input = "findByFirstnameAndLastname";
@@ -71,7 +140,7 @@ class N1qlQueryCreatorTests {
converter); converter);
Query query = creator.createQuery(); Query query = creator.createQuery();
assertEquals(query.export(), " WHERE " + where("firstname").is("John").and("lastname").is("Doe").export()); assertEquals(" WHERE " + where("firstname").is("John").and("lastname").is("Doe").export(), query.export());
} }
private ParameterAccessor getAccessor(Parameters<?, ?> params, Object... values) { private ParameterAccessor getAccessor(Parameters<?, ?> params, Object... values) {