diff --git a/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java b/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java index df4f6d3a6..7f1305812 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors + * Copyright 2012-2021 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. @@ -15,25 +15,29 @@ */ package org.springframework.data.couchbase.core.query; +import static org.springframework.data.couchbase.core.query.N1QLExpression.x; + import java.util.ArrayList; import java.util.Formatter; import java.util.LinkedList; import java.util.List; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.lang.Nullable; + 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; /** * @author Michael Nitschinger * @author Michael Reiche + * @author Mauro Monti */ public class QueryCriteria implements QueryCriteriaDefinition { - private final String key; + private final N1QLExpression key; /** * Holds the chain itself, the current operator being always the last one. */ @@ -46,16 +50,16 @@ public class QueryCriteria implements QueryCriteriaDefinition { private Object[] value; private String format; - QueryCriteria(List chain, String key, Object[] value, ChainOperator chainOperator) { + QueryCriteria(List chain, N1QLExpression key, Object[] value, ChainOperator chainOperator) { this(chain, key, value, chainOperator, null, null); } - QueryCriteria(List chain, String key, Object value, ChainOperator chainOperator) { + QueryCriteria(List chain, N1QLExpression key, Object value, ChainOperator chainOperator) { this(chain, key, new Object[] { value }, chainOperator, null, null); } - QueryCriteria(List chain, String key, Object[] value, ChainOperator chainOperator, String operator, - String format) { + QueryCriteria(List chain, N1QLExpression key, Object[] value, ChainOperator chainOperator, + String operator, String format) { this.criteriaChain = chain; criteriaChain.add(this); this.key = key; @@ -70,9 +74,16 @@ Object[] getValue() { } /** - * Static factory method to create a Criteria using the provided key. + * Static factory method to create a Criteria using the provided String key. */ public static QueryCriteria where(String key) { + return where(x(key)); + } + + /** + * Static factory method to create a Criteria using the provided N1QLExpression key. + */ + public static QueryCriteria where(N1QLExpression key) { return new QueryCriteria(new ArrayList<>(), key, null, null); } @@ -83,6 +94,10 @@ private static QueryCriteria wrap(QueryCriteria criteria) { } public QueryCriteria and(String key) { + return and(x(key)); + } + + public QueryCriteria and(N1QLExpression key) { return new QueryCriteria(this.criteriaChain, key, null, ChainOperator.AND); } @@ -90,14 +105,18 @@ public QueryCriteria and(QueryCriteria criteria) { return new QueryCriteria(this.criteriaChain, null, criteria, ChainOperator.AND); } - public QueryCriteria or(QueryCriteria criteria) { - return new QueryCriteria(this.criteriaChain, null, criteria, ChainOperator.OR); + public QueryCriteria or(String key) { + return or(x(key)); } - public QueryCriteria or(String key) { + public QueryCriteria or(N1QLExpression key) { return new QueryCriteria(this.criteriaChain, key, null, ChainOperator.OR); } + public QueryCriteria or(QueryCriteria criteria) { + return new QueryCriteria(this.criteriaChain, null, criteria, ChainOperator.OR); + } + public QueryCriteria eq(@Nullable Object o) { return is(o); } @@ -343,7 +362,7 @@ public String export() { // used only by tests */ private StringBuilder exportSingle(StringBuilder sb, int[] paramIndexPtr, JsonValue parameters, CouchbaseConverter converter) { - String fieldName = maybeBackTic(key); + String fieldName = key == null ? null : key.toString(); // maybeBackTic(key); int valueLen = value == null ? 0 : value.length; Object[] v = new Object[valueLen + 2]; v[0] = fieldName; @@ -377,7 +396,7 @@ private StringBuilder exportSingle(StringBuilder sb, int[] paramIndexPtr, JsonVa * @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, + private String maybeWrapValue(N1QLExpression key, Object value, int[] paramIndexPtr, JsonValue parameters, CouchbaseConverter converter) { if (paramIndexPtr != null) { if (paramIndexPtr[0] >= 0) { @@ -397,10 +416,10 @@ private String maybeWrapValue(String key, Object value, int[] paramIndexPtr, Jso JsonObject params = (JsonObject) parameters; // from StringBasedN1qlQueryParser.getNamedPlaceholderValues() try { - params.put(key, convert(converter, value)); + params.put(key.toString(), convert(converter, value)); } catch (InvalidArgumentException iae) { if (value instanceof Object[]) { - params.put(key, JsonArray.from((Object[]) value)); + params.put(key.toString(), JsonArray.from((Object[]) value)); } else { throw iae; } @@ -416,7 +435,7 @@ private String maybeWrapValue(String key, Object value, int[] paramIndexPtr, Jso } else if (value == null) { return "null"; } else if (value instanceof Object[]) { // convert array into sequence of comma-separated values - StringBuffer l = new StringBuffer(); + StringBuilder l = new StringBuilder(); l.append("["); Object[] array = (Object[]) value; for (int i = 0; i < array.length; i++) { diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlCountQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlCountQueryCreator.java index d78bd3406..04fd39093 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlCountQueryCreator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlCountQueryCreator.java @@ -118,6 +118,11 @@ public Pageable first() { return delegate.first(); } + @Override + public Pageable withPage(int i) { + return new CountPageable(delegate.withPage(i)); + } + public boolean hasPrevious() { return delegate.hasPrevious(); } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java index be4b21375..384106335 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors + * Copyright 2012-2021 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. @@ -15,6 +15,10 @@ */ package org.springframework.data.couchbase.repository.query; +import static org.springframework.data.couchbase.core.query.N1QLExpression.i; +import static org.springframework.data.couchbase.core.query.N1QLExpression.meta; +import static org.springframework.data.couchbase.core.query.N1QLExpression.path; +import static org.springframework.data.couchbase.core.query.N1QLExpression.x; import static org.springframework.data.couchbase.core.query.QueryCriteria.where; import java.util.Iterator; @@ -22,6 +26,7 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.query.N1QLExpression; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.query.QueryCriteria; import org.springframework.data.domain.Sort; @@ -36,33 +41,39 @@ /** * @author Michael Nitschinger * @author Michael Reiche + * @author Mauro Monti */ public class N1qlQueryCreator extends AbstractQueryCreator { + private static final String META_ID_PROPERTY = "id"; + private static final String META_CAS_PROPERTY = "cas"; + private final ParameterAccessor accessor; private final MappingContext context; private final QueryMethod queryMethod; private final CouchbaseConverter converter; + private final String bucketName; - public N1qlQueryCreator(final PartTree tree, final ParameterAccessor accessor, QueryMethod queryMethod, - CouchbaseConverter converter) { + public N1qlQueryCreator(final PartTree tree, final ParameterAccessor accessor, final QueryMethod queryMethod, + final CouchbaseConverter converter, final String bucketName) { super(tree, accessor); this.accessor = accessor; this.context = converter.getMappingContext(); this.queryMethod = queryMethod; this.converter = converter; + this.bucketName = bucketName; } @Override protected QueryCriteria create(final Part part, final Iterator iterator) { PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); CouchbasePersistentProperty property = path.getLeafProperty(); - return from(part, property, where(path.toDotPath(cvtr)), iterator); + return from(part, property, where(addMetaIfRequired(path, property)), iterator); } static Converter cvtr = ( - source) -> new StringBuilder(source.getName().length() + 2).append('`').append(source.getName()).append('`') - .toString(); + source) -> new StringBuilder(source.getFieldName().length() + 2).append('`').append(source.getFieldName()) + .append('`').toString(); @Override protected QueryCriteria and(final Part part, final QueryCriteria base, final Iterator iterator) { @@ -73,7 +84,7 @@ protected QueryCriteria and(final Part part, final QueryCriteria base, final Ite PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); CouchbasePersistentProperty property = path.getLeafProperty(); - return from(part, property, base.and(path.toDotPath()), iterator); + return from(part, property, base.and(addMetaIfRequired(path, property)), iterator); } @Override @@ -149,4 +160,16 @@ private QueryCriteria from(final Part part, final CouchbasePersistentProperty pr } } + private N1QLExpression addMetaIfRequired( + final PersistentPropertyPath persistentPropertyPath, + final CouchbasePersistentProperty property) { + if (property.isIdProperty()) { + return path(meta(i(bucketName)), i(META_ID_PROPERTY)); + } + if (property.isVersionProperty()) { + return path(meta(i(bucketName)), i(META_CAS_PROPERTY)); + } + return x(persistentPropertyPath.toDotPath(cvtr)); + } + } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlRepositoryQueryExecutor.java b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlRepositoryQueryExecutor.java index b1400bbb8..4cbf6c9de 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlRepositoryQueryExecutor.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlRepositoryQueryExecutor.java @@ -70,7 +70,7 @@ public Object execute(final Object[] parameters) { SPEL_PARSER, evaluationContextProvider, namedQueries).createQuery(); } else { final PartTree tree = new PartTree(queryMethod.getName(), domainClass); - query = new N1qlQueryCreator(tree, accessor, queryMethod, operations.getConverter()).createQuery(); + query = new N1qlQueryCreator(tree, accessor, queryMethod, operations.getConverter(), operations.getBucketName()).createQuery(); } ExecutableFindByQueryOperation.ExecutableFindByQuery operation = (ExecutableFindByQueryOperation.ExecutableFindByQuery) operations diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeCouchbaseQuery.java index 006a4cfa1..bef95ec1b 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeCouchbaseQuery.java @@ -72,7 +72,8 @@ public PartTree getTree() { @Override protected Query createQuery(ParametersParameterAccessor accessor) { - N1qlQueryCreator creator = new N1qlQueryCreator(tree, accessor, getQueryMethod(), converter); + N1qlQueryCreator creator = new N1qlQueryCreator(tree, accessor, getQueryMethod(), converter, + getOperations().getBucketName()); Query query = creator.createQuery(); if (tree.isLimiting()) { @@ -88,7 +89,8 @@ protected Query createQuery(ParametersParameterAccessor accessor) { */ @Override protected Query createCountQuery(ParametersParameterAccessor accessor) { - return new N1qlQueryCreator(tree, accessor, getQueryMethod(), converter).createQuery(); + return new N1qlQueryCreator(tree, accessor, getQueryMethod(), converter, getOperations().getBucketName()) + .createQuery(); } /* diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeCouchbaseQuery.java index fd60ad402..4c1c4faad 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactivePartTreeCouchbaseQuery.java @@ -73,7 +73,8 @@ public PartTree getTree() { @Override protected Query createQuery(ParametersParameterAccessor accessor) { - N1qlQueryCreator creator = new N1qlQueryCreator(tree, accessor, getQueryMethod(), converter); + N1qlQueryCreator creator = new N1qlQueryCreator(tree, accessor, getQueryMethod(), converter, + getOperations().getBucketName()); Query query = creator.createQuery(); if (tree.isLimiting()) { @@ -91,7 +92,8 @@ protected Query createQuery(ParametersParameterAccessor accessor) { */ @Override protected Query createCountQuery(ParametersParameterAccessor accessor) { - Query query = new N1qlQueryCreator(tree, accessor, getQueryMethod(), converter).createQuery(); + Query query = new N1qlQueryCreator(tree, accessor, getQueryMethod(), converter, getOperations().getBucketName()) + .createQuery(); if (LOG.isDebugEnabled()) { LOG.debug("Created query {} for * fields.", query.export()); } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringBasedCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringBasedCouchbaseQuery.java index 9626e4644..3296397a8 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringBasedCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringBasedCouchbaseQuery.java @@ -79,6 +79,7 @@ protected Query createQuery(ParametersParameterAccessor accessor) { StringN1qlQueryCreator creator = new StringN1qlQueryCreator(accessor, getQueryMethod(), getOperations().getConverter(), getOperations().getBucketName(), expressionParser, evaluationContextProvider, namedQueries); + Query query = creator.createQuery(); if (LOG.isDebugEnabled()) { diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java index d19f185f9..1e7d8e29a 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors + * Copyright 2021 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. @@ -15,9 +15,11 @@ */ package org.springframework.data.couchbase.repository.query; -import com.couchbase.client.java.json.JsonArray; -import com.couchbase.client.java.json.JsonObject; -import com.couchbase.client.java.json.JsonValue; +import static org.springframework.data.couchbase.core.query.N1QLExpression.x; +import static org.springframework.data.couchbase.core.query.QueryCriteria.where; + +import java.util.Iterator; + import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.query.N1QLExpression; @@ -36,12 +38,13 @@ import org.springframework.data.repository.query.parser.PartTree; import org.springframework.expression.spel.standard.SpelExpressionParser; -import java.util.Iterator; - -import static org.springframework.data.couchbase.core.query.QueryCriteria.where; +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.json.JsonObject; +import com.couchbase.client.java.json.JsonValue; /** * @author Michael Reiche + * @author Mauro Monti */ public class StringN1qlQueryCreator extends AbstractQueryCreator { @@ -98,10 +101,9 @@ protected String getTypeValue() { @Override protected QueryCriteria create(final Part part, final Iterator iterator) { - PersistentPropertyPath path = context.getPersistentPropertyPath( - part.getProperty()); + PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); CouchbasePersistentProperty property = path.getLeafProperty(); - return from(part, property, where(path.toDotPath()), iterator); + return from(part, property, where(x(path.toDotPath())), iterator); } @Override @@ -110,11 +112,10 @@ protected QueryCriteria and(final Part part, final QueryCriteria base, final Ite return create(part, iterator); } - PersistentPropertyPath path = context.getPersistentPropertyPath( - part.getProperty()); + PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); CouchbasePersistentProperty property = path.getLeafProperty(); - return from(part, property, base.and(path.toDotPath()), iterator); + return from(part, property, base.and(x(path.toDotPath())), iterator); } @Override @@ -139,10 +140,10 @@ private QueryCriteria from(final Part part, final CouchbasePersistentProperty pr final Part.Type type = part.getType(); switch (type) { - case SIMPLE_PROPERTY: - return criteria; // this will be the dummy from PartTree - default: - throw new IllegalArgumentException("Unsupported keyword!"); + case SIMPLE_PROPERTY: + return criteria; // this will be the dummy from PartTree + default: + throw new IllegalArgumentException("Unsupported keyword!"); } } diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java index 30a0a96e1..1233808af 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateQueryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors + * Copyright 2012-2021 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. @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE; import static org.springframework.data.couchbase.config.BeanNames.REACTIVE_COUCHBASE_TEMPLATE; +import static org.springframework.data.couchbase.core.query.N1QLExpression.i; import java.time.Instant; import java.time.temporal.TemporalAccessor; @@ -60,6 +61,7 @@ * @author Michael Nitschinger * @author Michael Reiche * @author Haris Alesevic + * @author Mauro Monti */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) class CouchbaseTemplateQueryIntegrationTests extends JavaIntegrationTests { @@ -124,7 +126,7 @@ void findByMatchingQuery() { couchbaseTemplate.upsertById(User.class).all(Arrays.asList(user1, user2, specialUser)); - Query specialUsers = new Query(QueryCriteria.where("firstname").like("special")); + Query specialUsers = new Query(QueryCriteria.where(i("firstname")).like("special")); final List foundUsers = couchbaseTemplate.findByQuery(User.class) .withConsistency(QueryScanConsistency.REQUEST_PLUS).matching(specialUsers).all(); @@ -206,7 +208,7 @@ void removeByMatchingQuery() { assertTrue(couchbaseTemplate.existsById().one(user2.getId())); assertTrue(couchbaseTemplate.existsById().one(specialUser.getId())); - Query nonSpecialUsers = new Query(QueryCriteria.where("firstname").notLike("special")); + Query nonSpecialUsers = new Query(QueryCriteria.where(i("firstname")).notLike("special")); couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS) .matching(nonSpecialUsers).all(); diff --git a/src/test/java/org/springframework/data/couchbase/core/query/QueryCriteriaTests.java b/src/test/java/org/springframework/data/couchbase/core/query/QueryCriteriaTests.java index 0dcbd991c..ca1e06afd 100644 --- a/src/test/java/org/springframework/data/couchbase/core/query/QueryCriteriaTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/query/QueryCriteriaTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2021 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. @@ -16,57 +16,56 @@ package org.springframework.data.couchbase.core.query; -import static org.junit.jupiter.api.Assertions.*; -import static org.springframework.data.couchbase.core.query.QueryCriteria.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.springframework.data.couchbase.core.query.N1QLExpression.i; +import static org.springframework.data.couchbase.core.query.N1QLExpression.meta; +import static org.springframework.data.couchbase.core.query.N1QLExpression.path; +import static org.springframework.data.couchbase.core.query.N1QLExpression.x; +import static org.springframework.data.couchbase.core.query.QueryCriteria.where; +import static org.springframework.data.couchbase.repository.query.support.N1qlUtils.escapedBucket; -import com.couchbase.client.java.json.JsonArray; -import com.couchbase.client.java.json.JsonObject; 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; +import com.couchbase.client.java.json.JsonArray; +/** + * @author Mauro Monti + */ class QueryCriteriaTests { @Test void testSimpleCriteria() { - QueryCriteria c = where("name").is("Bubba"); + QueryCriteria c = where(i("name")).is("Bubba"); assertEquals("`name` = \"Bubba\"", c.export()); } @Test public void testNullValue() { - QueryCriteria c = where("name").is(null); + QueryCriteria c = where(i("name")).is(null); assertEquals("`name` = null", c.export()); } @Test void testSimpleNumber() { - QueryCriteria c = where("name").is(5); + QueryCriteria c = where(i("name")).is(5); assertEquals("`name` = 5", c.export()); } @Test void testNotEqualCriteria() { - QueryCriteria c = where("name").ne("Bubba"); + QueryCriteria c = where(i("name")).ne("Bubba"); assertEquals("`name` != \"Bubba\"", c.export()); } @Test void testChainedCriteria() { - QueryCriteria c = where("name").is("Bubba").and("age").lt(21).or("country").is("Austria"); + QueryCriteria c = where(i("name")).is("Bubba").and(i("age")).lt(21).or(i("country")).is("Austria"); assertEquals("`name` = \"Bubba\" and `age` < 21 or `country` = \"Austria\"", c.export()); } @Test void testNestedAndCriteria() { - QueryCriteria c = where("name").is("Bubba").and(where("age").gt(12).or("country").is("Austria")); + QueryCriteria c = where(i("name")).is("Bubba").and(where(i("age")).gt(12).or(i("country")).is("Austria")); 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()); @@ -74,7 +73,7 @@ void testNestedAndCriteria() { @Test void testNestedOrCriteria() { - QueryCriteria c = where("name").is("Bubba").or(where("age").gt(12).or("country").is("Austria")); + QueryCriteria c = where(i("name")).is("Bubba").or(where(i("age")).gt(12).or(i("country")).is("Austria")); 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()); @@ -82,45 +81,45 @@ void testNestedOrCriteria() { @Test void testNestedNotIn() { - QueryCriteria c = where("name").is("Bubba").or(where("age").gt(12).or("country").is("Austria")) - .and(where("state").notIn(new String[] { "Alabama", "Florida" })); + QueryCriteria c = where(i("name")).is("Bubba").or(where(i("age")).gt(12).or(i("country")).is("Austria")) + .and(where(i("state")).notIn(new String[] { "Alabama", "Florida" })); assertEquals("`name` = \"Bubba\" or (`age` > 12 or `country` = \"Austria\") and " + "(not( (`state` in ( [\"Alabama\",\"Florida\"] )) ))", c.export()); } @Test void testLt() { - QueryCriteria c = where("name").lt("Couch"); + QueryCriteria c = where(i("name")).lt("Couch"); assertEquals("`name` < \"Couch\"", c.export()); } @Test void testLte() { - QueryCriteria c = where("name").lte("Couch"); + QueryCriteria c = where(i("name")).lte("Couch"); assertEquals("`name` <= \"Couch\"", c.export()); } @Test void testGt() { - QueryCriteria c = where("name").gt("Couch"); + QueryCriteria c = where(i("name")).gt("Couch"); assertEquals("`name` > \"Couch\"", c.export()); } @Test void testGte() { - QueryCriteria c = where("name").gte("Couch"); + QueryCriteria c = where(i("name")).gte("Couch"); assertEquals("`name` >= \"Couch\"", c.export()); } @Test void testNe() { - QueryCriteria c = where("name").ne("Couch"); + QueryCriteria c = where(i("name")).ne("Couch"); assertEquals("`name` != \"Couch\"", c.export()); } @Test void testStartingWith() { - QueryCriteria c = where("name").startingWith("Cou"); + QueryCriteria c = where(i("name")).startingWith("Cou"); assertEquals("`name` like (\"Cou\"||\"%\")", c.export()); } @@ -128,99 +127,99 @@ void testStartingWith() { * startingWith() cannot be a QueryCriteria @Test void testStartingWithExpr() { - QueryCriteria c = where("name").startingWith(where("name").plus("xxx")); + QueryCriteria c = where(i("name")).startingWith(where(i("name")).plus("xxx")); assertEquals("`name` like (((`name` || "xxx") || ""%""))", c.export()); } */ @Test void testEndingWith() { - QueryCriteria c = where("name").endingWith("ouch"); + QueryCriteria c = where(i("name")).endingWith("ouch"); assertEquals("`name` like (\"%\"||\"ouch\")", c.export()); } @Test void testEndingWithExpr() { - QueryCriteria c = where("name").endingWith(where("name").plus("xxx")); + QueryCriteria c = where(i("name")).endingWith(where(i("name")).plus("xxx")); assertEquals("`name` like (\"%\"||((`name` || \"xxx\")))", c.export()); } @Test void testRegex() { - QueryCriteria c = where("name").regex("C.*h"); + QueryCriteria c = where(i("name")).regex("C.*h"); assertEquals("regexp_like(`name`, \"C.*h\")", c.export()); } @Test void testContaining() { - QueryCriteria c = where("name").containing("ouch"); + QueryCriteria c = where(i("name")).containing("ouch"); assertEquals("contains(`name`, \"ouch\")", c.export()); } @Test void testNotContaining() { - QueryCriteria c = where("name").notContaining("Elvis"); + QueryCriteria c = where(i("name")).notContaining("Elvis"); assertEquals("not( (contains(`name`, \"Elvis\")) )", c.export()); } @Test void testLike() { - QueryCriteria c = where("name").like("%ouch%"); + QueryCriteria c = where(i("name")).like("%ouch%"); assertEquals("`name` like \"%ouch%\"", c.export()); } @Test void testNotLike() { - QueryCriteria c = where("name").notLike("%Elvis%"); + QueryCriteria c = where(i("name")).notLike("%Elvis%"); assertEquals("not(`name` like \"%Elvis%\")", c.export()); } @Test void testIsNull() { - QueryCriteria c = where("name").isNull(); + QueryCriteria c = where(i("name")).isNull(); assertEquals("`name` is null", c.export()); } @Test void testIsNotNull() { - QueryCriteria c = where("name").isNotNull(); + QueryCriteria c = where(i("name")).isNotNull(); assertEquals("`name` is not null", c.export()); } @Test void testIsMissing() { - QueryCriteria c = where("name").isMissing(); + QueryCriteria c = where(i("name")).isMissing(); assertEquals("`name` is missing", c.export()); } @Test void testIsNotMissing() { - QueryCriteria c = where("name").isNotMissing(); + QueryCriteria c = where(i("name")).isNotMissing(); assertEquals("`name` is not missing", c.export()); } @Test void testIsValued() { - QueryCriteria c = where("name").isValued(); + QueryCriteria c = where(i("name")).isValued(); assertEquals("`name` is valued", c.export()); } @Test void testIsNotValued() { - QueryCriteria c = where("name").isNotValued(); + QueryCriteria c = where(i("name")).isNotValued(); assertEquals("`name` is not valued", c.export()); } @Test void testBetween() { - QueryCriteria c = where("name").between("Davis", "Gump"); + QueryCriteria c = where(i("name")).between("Davis", "Gump"); assertEquals("`name` between \"Davis\" and \"Gump\"", c.export()); } @Test void testIn() { String[] args = new String[] { "gump", "davis" }; - QueryCriteria c = where("name").in(args); + QueryCriteria c = where(i("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)); @@ -230,7 +229,7 @@ void testIn() { @Test void testNotIn() { String[] args = new String[] { "gump", "davis" }; - QueryCriteria c = where("name").notIn(args); + QueryCriteria c = where(i("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)); @@ -239,16 +238,32 @@ void testNotIn() { @Test void testTrue() { - QueryCriteria c = where("name").TRUE(); + QueryCriteria c = where(i("name")).TRUE(); assertEquals("`name`", c.export()); } @Test void testFalse() { - QueryCriteria c = where("name").FALSE(); + QueryCriteria c = where(i("name")).FALSE(); assertEquals("not( (`name`) )", c.export()); } + @Test // https://github.com/spring-projects/spring-data-couchbase/issues/1066 + void testCriteriaCorrectlyEscapedWhenUsingMetaOnLHS() { + final String bucketName = "sample-bucket"; + final String version = "1611287177404088320"; + QueryCriteria criteria = QueryCriteria.where(path(meta(escapedBucket(bucketName)), "cas")).eq(x(version)); + assertEquals("META(`" + bucketName + "`).cas = " + x(version), criteria.export()); + } + + @Test // https://github.com/spring-projects/spring-data-couchbase/issues/1066 + void testCriteriaCorrectlyEscapedWhenUsingMetaOnRHS() { + final String bucketName = "sample-bucket"; + final String version = "1611287177404088320"; + QueryCriteria criteria = QueryCriteria.where(x(version)).eq(path(meta(escapedBucket(bucketName)), "cas")); + assertEquals(x(version) + " = META(`" + bucketName + "`).cas", criteria.export()); + } + private String arrayToString(Object[] array) { StringBuilder sb = new StringBuilder(); if (array != null) { diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java b/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java index 023afa5b1..63293b335 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java @@ -19,12 +19,12 @@ import java.util.List; import org.springframework.data.couchbase.repository.CouchbaseRepository; -import com.couchbase.client.java.json.JsonArray; import org.springframework.data.couchbase.repository.Query; -import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import com.couchbase.client.java.json.JsonArray; + /** * User Repository for tests * @@ -47,4 +47,8 @@ public interface UserRepository extends CouchbaseRepository { @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and (firstname = $first or lastname = $last)") List getByFirstnameOrLastname(@Param("first") String firstname, @Param("last") String lastname); + + List findByIdIsNotNullAndFirstnameEquals(String firstname); + + List findByVersionEqualsAndFirstnameEquals(Long version, String firstname); } diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java index a71a4fbe5..484cd0e26 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -16,9 +16,12 @@ package org.springframework.data.couchbase.repository; -import static java.util.Arrays.*; -import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.*; +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; import java.util.Arrays; @@ -49,14 +52,12 @@ import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; -import org.springframework.data.util.StreamUtils; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.IndexExistsException; -import reactor.core.publisher.Flux; /** * Repository tests @@ -173,7 +174,9 @@ void count() { try { - airportRepository.saveAll( Arrays.stream(iatas).map((iata) -> new Airport("airports::"+iata, iata, iata.toLowerCase(Locale.ROOT))).collect(Collectors.toSet())); + airportRepository.saveAll( + Arrays.stream(iatas).map((iata) -> new Airport("airports::" + iata, iata, iata.toLowerCase(Locale.ROOT))) + .collect(Collectors.toSet())); Long count = airportRepository.countFancyExpression(asList("JFK"), asList("jfk"), false); assertEquals(1, count); @@ -198,7 +201,8 @@ void count() { assertEquals(0, airportCount); } finally { - airportRepository.deleteAllById(Arrays.stream(iatas).map((iata) -> "airports::"+iata).collect(Collectors.toSet())); + airportRepository + .deleteAllById(Arrays.stream(iatas).map((iata) -> "airports::" + iata).collect(Collectors.toSet())); } } @@ -295,7 +299,6 @@ void deleteAllById() { airportRepository.deleteAllById(asList(vienna.getId(), losAngeles.getId())); - assertThat(airportRepository.findAll()).containsExactly(frankfurt); } finally { airportRepository.deleteAll(); @@ -305,9 +308,7 @@ void deleteAllById() { private void sleep(int millis) { try { Thread.sleep(millis); // so they are executed out-of-order - } catch (InterruptedException ie) { - ; - } + } catch (InterruptedException ie) {} } @Configuration diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTests.java index f3bbd618d..65f99fa93 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2021 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. @@ -15,17 +15,15 @@ */ package org.springframework.data.couchbase.repository.query; -import static org.junit.jupiter.api.Assertions.*; -import static org.springframework.data.couchbase.core.query.QueryCriteria.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.springframework.data.couchbase.core.query.N1QLExpression.i; +import static org.springframework.data.couchbase.core.query.N1QLExpression.x; +import static org.springframework.data.couchbase.core.query.QueryCriteria.where; 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.Test; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; @@ -37,22 +35,31 @@ import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserRepository; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.repository.query.*; +import org.springframework.data.repository.query.DefaultParameters; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.parser.PartTree; +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.json.JsonObject; + /** * @author Michael Nitschinger * @author Michael Reiche + * @author Mauro Monti */ class N1qlQueryCreatorTests { MappingContext, CouchbasePersistentProperty> context; CouchbaseConverter converter; + String bucketName; @BeforeEach public void beforeEach() { context = new CouchbaseMappingContext(); converter = new MappingCouchbaseConverter(context); + bucketName = "sample-bucket"; } @Test @@ -61,11 +68,11 @@ void createsQueryCorrectly() throws Exception { PartTree tree = new PartTree(input, User.class); Method method = UserRepository.class.getMethod(input, String.class); - N1qlQueryCreator creator = new N1qlQueryCreator(tree, getAccessor(getParameters(method), "Oliver"), null, - converter); + N1qlQueryCreator creator = new N1qlQueryCreator(tree, getAccessor(getParameters(method), "Oliver"), null, converter, + bucketName); Query query = creator.createQuery(); - assertEquals(query.export(), " WHERE " + where("firstname").is("Oliver").export()); + assertEquals(query.export(), " WHERE " + where(i("firstname")).is("Oliver").export()); } @Test @@ -73,9 +80,10 @@ 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")); + Query expected = (new Query()).addCriteria(where(i("firstname")).in("Oliver", "Charles")); N1qlQueryCreator creator = new N1qlQueryCreator(tree, - getAccessor(getParameters(method), new Object[] { new Object[] { "Oliver", "Charles" } }), null, converter); + getAccessor(getParameters(method), new Object[] { new Object[] { "Oliver", "Charles" } }), null, converter, + bucketName); Query query = creator.createQuery(); // Query expected = (new Query()).addCriteria(where("firstname").in("Oliver", "Charles")); @@ -97,10 +105,10 @@ void queryParametersJsonArray() throws Exception { jsonArray.add("Oliver"); jsonArray.add("Charles"); N1qlQueryCreator creator = new N1qlQueryCreator(tree, getAccessor(getParameters(method), jsonArray), null, - converter); + converter, bucketName); Query query = creator.createQuery(); - Query expected = (new Query()).addCriteria(where("firstname").in("Oliver", "Charles")); + Query expected = (new Query()).addCriteria(where(i("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); @@ -118,10 +126,10 @@ void queryParametersList() throws Exception { list.add("Oliver"); list.add("Charles"); N1qlQueryCreator creator = new N1qlQueryCreator(tree, getAccessor(getParameters(method), new Object[] { list }), - null, converter); + null, converter, bucketName); Query query = creator.createQuery(); - Query expected = (new Query()).addCriteria(where("firstname").in("Oliver", "Charles")); + Query expected = (new Query()).addCriteria(where(i("firstname")).in("Oliver", "Charles")); assertEquals(expected.export(new int[1]), query.export(new int[1])); JsonObject expectedOptions = JsonObject.create(); @@ -137,10 +145,38 @@ void createsAndQueryCorrectly() throws Exception { PartTree tree = new PartTree(input, User.class); Method method = UserRepository.class.getMethod(input, String.class, String.class); N1qlQueryCreator creator = new N1qlQueryCreator(tree, getAccessor(getParameters(method), "John", "Doe"), null, - converter); + converter, bucketName); + Query query = creator.createQuery(); + + assertEquals(" WHERE " + where(i("firstname")).is("John").and(i("lastname")).is("Doe").export(), query.export()); + } + + @Test // https://github.com/spring-projects/spring-data-couchbase/issues/1072 + void createsQueryFindByIdIsNotNullAndFirstname() throws Exception { + String input = "findByIdIsNotNullAndFirstnameEquals"; + PartTree tree = new PartTree(input, User.class); + Method method = UserRepository.class.getMethod(input, String.class); + + N1qlQueryCreator creator = new N1qlQueryCreator(tree, getAccessor(getParameters(method), "Oliver"), null, converter, + bucketName); + Query query = creator.createQuery(); + + assertEquals(query.export(), + " WHERE " + where(x("META(`" + bucketName + "`).`id`")).isNotNull().and(i("firstname")).is("Oliver").export()); + } + + @Test // https://github.com/spring-projects/spring-data-couchbase/issues/1072 + void createsQueryFindByVersionEqualsAndAndFirstname() throws Exception { + String input = "findByVersionEqualsAndFirstnameEquals"; + PartTree tree = new PartTree(input, User.class); + Method method = UserRepository.class.getMethod(input, Long.class, String.class); + + N1qlQueryCreator creator = new N1qlQueryCreator(tree, + getAccessor(getParameters(method), 1611287177404088320L, "Oliver"), null, converter, bucketName); Query query = creator.createQuery(); - assertEquals(" WHERE " + where("firstname").is("John").and("lastname").is("Doe").export(), query.export()); + assertEquals(query.export(), " WHERE " + where(x("META(`" + bucketName + "`).`cas`")).is(1611287177404088320L) + .and(i("firstname")).is("Oliver").export()); } private ParameterAccessor getAccessor(Parameters params, Object... values) {