diff --git a/src/integration/java/org/springframework/data/couchbase/repository/N1qlCouchbaseRepositoryTests.java b/src/integration/java/org/springframework/data/couchbase/repository/N1qlCouchbaseRepositoryTests.java index 697878d6..caeff2d5 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/N1qlCouchbaseRepositoryTests.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/N1qlCouchbaseRepositoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-2017 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. @@ -45,6 +45,7 @@ import java.util.List; * This tests PaginAndSortingRepository features in the Couchbase connector. * * @author Simon Baslé + * @author Subhashni Balakrishnan */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = IntegrationTestApplicationConfig.class) @@ -185,4 +186,12 @@ public class N1qlCouchbaseRepositoryTests { Sort sort = new Sort(Sort.Direction.DESC, "attendees"); List parties = partyRepository.findParties(sort); } + + @Test + public void testDeleteQuery() { + partyRepository.save(new Party("testDeleteQuery", "delete", "delete", null, 0, null)); + List partyList = partyRepository.removeByDescriptionOrName("delete", "delete"); + assertTrue(partyList.size() == 1); + } + } diff --git a/src/integration/java/org/springframework/data/couchbase/repository/N1qlPlaceholderTests.java b/src/integration/java/org/springframework/data/couchbase/repository/N1qlPlaceholderTests.java index 79aa9b5f..d103c55d 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/N1qlPlaceholderTests.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/N1qlPlaceholderTests.java @@ -120,4 +120,19 @@ public class N1qlPlaceholderTests { "one over the other in findAllWithMixedParamsInQuery", e.getMessage()); } } + + @Test + public void deleteQueryTest() { + String included = "90"; + String excluded = "New Year"; + int max = 200; + List result = partyRepository.removeWithPositionalParams(excluded, included, max); + + assertEquals(10, result.size()); + for (Party party : result) { + assertTrue(party.getDescription().contains(included)); + assertFalse(party.getDescription().contains(excluded)); + assertTrue(party.getAttendees() < max); + } + } } diff --git a/src/integration/java/org/springframework/data/couchbase/repository/PartyRepository.java b/src/integration/java/org/springframework/data/couchbase/repository/PartyRepository.java index dcec677b..e3b1dbeb 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/PartyRepository.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/PartyRepository.java @@ -1,3 +1,19 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.data.couchbase.repository; import java.util.Date; @@ -14,6 +30,7 @@ import org.springframework.data.repository.query.Param; /** * @author Simon Baslé + * @author Subhashni Balakrishnan */ @ViewIndexed(designDoc = "party", viewName = "all") @N1qlSecondaryIndexed(indexName = "party") @@ -59,10 +76,15 @@ public interface PartyRepository extends CouchbaseRepository { " AND `desc` NOT LIKE '%' || $1 || '%'") List findAllWithPositionalParams(String ex, String inc, long minimumAttendees); + @Query("#{#n1ql.delete} WHERE #{#n1ql.filter} AND `desc` LIKE '%' || $2 || '%' AND attendees < $3" + + " AND `desc` NOT LIKE '%' || $1 || '%' #{#n1ql.returning}") + List removeWithPositionalParams(String ex, String inc, long minimumAttendees); + @Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND `desc` LIKE '%' || $2 || '%' AND attendees >= $3" + " AND `desc` NOT LIKE '%' || $1 || '%' AND `desc` != \"this is \\\"$excluded\\\"\"") List findAllWithPositionalParamsAndQuotedNamedParams(@Param("excluded") String ex, @Param("included") String inc, @Param("min") long min); List findByDescriptionOrName(String description, String name); + List removeByDescriptionOrName(String description, String name); } diff --git a/src/main/asciidoc/repository.adoc b/src/main/asciidoc/repository.adoc index 9064d4b4..cba88abd 100644 --- a/src/main/asciidoc/repository.adoc +++ b/src/main/asciidoc/repository.adoc @@ -113,7 +113,8 @@ While the exposed methods provide you with a great variety of access patterns, v === N1QL based querying As of version `4.0`, Couchbase Server ships with a new query language called `N1QL`. In `Spring-Data-Couchbase 2.0`, N1QL is the default way of doing queries and will allow you to fully derive queries from a method name. -Prerequisite is to have a N1QL-compatible cluster and to have created a PRIMARY INDEX on the bucket where the entities will be stored. +Prerequisite is to have a N1QL-compatible cluster and to have created a PRIMARY INDEX on the bucket where the entities will be stored. DML queries +are supported from Couchbase server version `4.1`. WARNING: If it is detected at configuration time that the cluster doesn't support N1QL while there are `@Query` annotated methods or non-annotated methods in your repository interface, a `UnsupportedCouchbaseFeatureException` will be thrown. @@ -142,6 +143,8 @@ A few N1QL-specific values are provided through SpEL: - `#n1ql.filter` in the WHERE clause adds a criteria matching the entity type with the field that Spring Data uses to store type information. - `#n1ql.bucket` will be replaced by the name of the bucket the entity is stored in, escaped in backticks. - `#n1ql.fields` will be replaced by the list of fields (eg. for a SELECT clause) necessary to reconstruct the entity. + - `#n1ql.delete` will be replaced by the `delete from` statement. + - `#n1ql.returning` will be replaced by returning clause needed for reconstructing entity. IMPORTANT: We recommend that you always use the `selectEntity` SpEL and a WHERE clause with a `filter` SpEL (since otherwise your query could be impacted by entities from other repositories). @@ -206,6 +209,15 @@ This could be useful to craft a query according to the role of the connected use "role = '?#{hasRole('ROLE_ADMIN') ? 'public_admin' : 'admin'}'") List findAllAdmins(); //only ROLE_ADMIN users will see hidden admins ---- + +Delete query example: +[source,java] +---- +@Query("#{#n1ql.delete} WHERE #{#n1ql.filter} AND " + +"username = $1 #{#n1ql.returning}") +UserInfo removeUser(String username); +---- + **** The second method uses Spring-Data's query derivation mechanism to build a N1QL query from the method name and parameters. This will produce a query looking like this: `SELECT ... FROM ... WHERE firstName = "valueOfFnameAtRuntime"`. You can combine these criteria, even do a count with a name like `countByFirstname` or a limit with a name like `findFirst3ByLastname`... diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java index 38c512c8..b5d15ecd 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java @@ -123,10 +123,6 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery { protected Object executeDependingOnType(N1qlQuery query, N1qlQuery countQuery, QueryMethod queryMethod, Pageable pageable, Class typeToRead) { - if (queryMethod.isModifyingQuery()) { - throw new UnsupportedOperationException("Modifying queries not yet supported"); - } - if (queryMethod.isPageQuery()) { return executePaged(query, countQuery, pageable, typeToRead); } else if (queryMethod.isSliceQuery()) { diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ConvertingIterator.java b/src/main/java/org/springframework/data/couchbase/repository/query/ConvertingIterator.java index af00dae3..7c2bd27f 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ConvertingIterator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ConvertingIterator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors + * Copyright 2012-2017 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. @@ -23,8 +23,11 @@ import org.springframework.data.couchbase.core.convert.CouchbaseConverter; /** * An {@link Iterator Iterator<Object>} that {@link CouchbaseConverter#convertForWriteIfNeeded(Object) converts} * values to their stored Class if warranted. + * + * @author Simon Baslé + * @author Subhashni Balakrishnan */ -class ConvertingIterator implements Iterator { +public class ConvertingIterator implements Iterator { private final Iterator delegate; private final CouchbaseConverter converter; diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlMutateQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlMutateQueryCreator.java new file mode 100644 index 00000000..2794d32c --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlMutateQueryCreator.java @@ -0,0 +1,77 @@ +/* + * Copyright 2017 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.repository.query; + +import java.util.Iterator; +import com.couchbase.client.java.query.dsl.Expression; +import com.couchbase.client.java.query.dsl.path.*; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.repository.query.support.N1qlQueryCreatorUtils; +import org.springframework.data.couchbase.repository.query.support.N1qlUtils; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.parser.AbstractQueryCreator; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.PartTree; + +/** + * N1qlMutateQueryCreator allows to create queries for delete operations. + * + * See {@link N1qlQueryCreator} for part types supported + * + * @author Subhashni Balakrishnan + */ +public class N1qlMutateQueryCreator extends AbstractQueryCreator { + private final MutateWherePath mutateFrom; + private final CouchbaseConverter converter; + private final CouchbaseQueryMethod queryMethod; + private final ParameterAccessor accessor; + + public N1qlMutateQueryCreator(PartTree tree, ParameterAccessor parameters, MutateWherePath mutateFrom, + CouchbaseConverter converter, CouchbaseQueryMethod queryMethod) { + super(tree, parameters); + this.mutateFrom = mutateFrom; + this.converter = converter; + this.queryMethod = queryMethod; + this.accessor = parameters; + } + + @Override + protected Expression create(Part part, Iterator iterator) { + return N1qlQueryCreatorUtils.prepareExpression(this.converter, part, iterator); + } + + @Override + protected Expression and(Part part, Expression base, Iterator iterator) { + if (base == null) { + return create(part, iterator); + } + + return base.and(create(part, iterator)); + } + + @Override + protected Expression or(Expression base, Expression criteria) { + return base.or(criteria); + } + + @Override + protected MutateLimitPath complete(Expression criteria, Sort sort) { + Expression whereCriteria = N1qlUtils.createWhereFilterForEntity(criteria, this.converter, this.queryMethod.getEntityInformation()); + return mutateFrom.where(whereCriteria); + } +} \ No newline at end of file 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 15cb0bc8..f12ce98b 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 @@ -16,27 +16,19 @@ package org.springframework.data.couchbase.repository.query; -import static com.couchbase.client.java.query.dsl.Expression.s; -import static com.couchbase.client.java.query.dsl.Expression.x; - -import java.util.Collection; import java.util.Iterator; import com.couchbase.client.java.document.json.JsonArray; import com.couchbase.client.java.query.dsl.Expression; -import com.couchbase.client.java.query.dsl.functions.PatternMatchingFunctions; -import com.couchbase.client.java.query.dsl.functions.StringFunctions; import com.couchbase.client.java.query.dsl.path.LimitPath; import com.couchbase.client.java.query.dsl.path.OrderByPath; import com.couchbase.client.java.query.dsl.path.WherePath; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.repository.query.support.N1qlQueryCreatorUtils; import org.springframework.data.couchbase.repository.query.support.N1qlUtils; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.data.geo.GeoResult; -import org.springframework.data.mapping.context.PersistentPropertyPath; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.data.repository.query.parser.Part; @@ -88,6 +80,7 @@ import org.springframework.data.repository.query.parser.PartTree; *

* * @author Simon Baslé + * @author Subhashni Balakrishnan * @author Mark Paluch */ public class N1qlQueryCreator extends AbstractQueryCreator { @@ -108,7 +101,7 @@ public class N1qlQueryCreator extends AbstractQueryCreator iterator) { - return prepareExpression(part, iterator); + return N1qlQueryCreatorUtils.prepareExpression(converter, part, iterator); } @Override @@ -144,161 +137,4 @@ public class N1qlQueryCreator extends AbstractQueryCreator iterator) { - PersistentPropertyPath path = N1qlUtils.getPathWithAlternativeFieldNames( - this.converter, part.getProperty()); - ConvertingIterator parameterValues = new ConvertingIterator(iterator, converter); - - //get the whole doted path with fieldNames instead of potentially wrong propNames - String fieldNamePath = N1qlUtils.getDottedPathWithAlternativeFieldNames(path); - - //deal with ignore case - boolean ignoreCase = false; - Class leafType = converter.getWriteClassFor(path.getLeafProperty().getType()); - boolean isString = leafType == String.class; - if (part.shouldIgnoreCase() == Part.IgnoreCaseType.WHEN_POSSIBLE) { - ignoreCase = isString; - } else if (part.shouldIgnoreCase() == Part.IgnoreCaseType.ALWAYS) { - if (!isString) { - throw new IllegalArgumentException(String.format("Part %s must be of type String but was %s", fieldNamePath, leafType)); - } - ignoreCase = true; - } - - return createExpression(part.getType(), fieldNamePath, ignoreCase, parameterValues); - } - - protected static Expression createExpression(Part.Type partType, String fieldNamePath, boolean ignoreCase, Iterator parameterValues) { - //create the left hand side of the expression, taking ignoreCase into account - Expression left = ignoreCase ? StringFunctions.lower(x(fieldNamePath)) : x(fieldNamePath); - - switch (partType) { - case BETWEEN: - return left.between(leftAndRight(parameterValues, ignoreCase)); - case IS_NOT_NULL: - return left.isNotNull(); - case IS_NULL: - return left.isNull(); - case NEGATING_SIMPLE_PROPERTY: - return left.ne(right(parameterValues, ignoreCase)); - case SIMPLE_PROPERTY: - return left.eq(right(parameterValues, ignoreCase)); - case BEFORE: - case LESS_THAN: - return left.lt(right(parameterValues, ignoreCase)); - case LESS_THAN_EQUAL: - return left.lte(right(parameterValues, ignoreCase)); - case GREATER_THAN_EQUAL: - return left.gte(right(parameterValues, ignoreCase)); - case AFTER: - case GREATER_THAN: - return left.gt(right(parameterValues, ignoreCase)); - case NOT_LIKE: - return left.notLike(right(parameterValues, ignoreCase)); - case LIKE: - return left.like(right(parameterValues, ignoreCase)); - case STARTING_WITH: - return left.like(like(parameterValues, ignoreCase, false, true)); - case ENDING_WITH: - return left.like(like(parameterValues, ignoreCase, true, false)); - case NOT_CONTAINING: - return left.notLike(like(parameterValues, ignoreCase, true, true)); - case CONTAINING: - return left.like(like(parameterValues, ignoreCase, true, true)); - case NOT_IN: - return left.notIn(rightArray(parameterValues)); - case IN: - return left.in(rightArray(parameterValues)); - case TRUE: - return left.eq(true); - case FALSE: - return left.eq(false); - case REGEX: - return regexp(fieldNamePath, parameterValues); - case EXISTS: - return left.isNotMissing(); - case WITHIN: - case NEAR: - default: - throw new IllegalArgumentException("Unsupported keyword in N1QL query derivation"); - } - } - - protected static Expression regexp(String left, Iterator parameterValues) { - Object next = parameterValues.next(); - - String pattern; - if (next == null) { - pattern = ""; - } else { - pattern = String.valueOf(next); - } - return PatternMatchingFunctions.regexpLike(left, pattern); - } - - protected static Expression leftAndRight(Iterator parameterValues, boolean ignoreCase) { - return right(parameterValues, ignoreCase).and(right(parameterValues, ignoreCase)); - } - - protected static Expression like(Iterator parameterValues, boolean ignoreCase, - boolean anyPrefix, boolean anySuffix) { - Object next = parameterValues.next(); - if (next == null) { - return Expression.NULL(); - } - - Expression converted; - if (next instanceof String) { - String pattern = (String) next; - if (anyPrefix) { - pattern = "%" + pattern; - } - if (anySuffix) { - pattern = pattern + "%"; - } - converted = s(pattern); - - } else { - converted = x(String.valueOf(next)); - } - - if (ignoreCase) { - return StringFunctions.lower(converted); - } - return converted; - } - - protected static Expression right(Iterator parameterValues, boolean ignoreCase) { - Object next = parameterValues.next(); - if (next == null) { - return Expression.NULL(); - } - - Expression converted; - if (next instanceof String) { - converted = s((String) next); - } else { - converted = x(String.valueOf(next)); - } - - if (ignoreCase) { - return StringFunctions.lower(converted); - } - return converted; - } - - protected static JsonArray rightArray(Iterator parameterValues) { - Object next = parameterValues.next(); - - Object[] values; - if (next instanceof Collection) { - values = ((Collection) next).toArray(); - } else if (next.getClass().isArray()) { - values = (Object[]) next; - } else { - values = new Object[] {next}; - } - return JsonArray.from(values); - } - } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java index 61bf88a4..708bce6b 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java @@ -16,9 +16,11 @@ package org.springframework.data.couchbase.repository.query; +import static com.couchbase.client.java.query.Delete.deleteFrom; import static com.couchbase.client.java.query.Select.select; import static com.couchbase.client.java.query.dsl.Expression.i; import static com.couchbase.client.java.query.dsl.functions.AggregateFunctions.count; +import static org.springframework.data.couchbase.repository.query.support.N1qlUtils.createReturningExpressionForDelete; import com.couchbase.client.java.document.json.JsonArray; import com.couchbase.client.java.document.json.JsonValue; import com.couchbase.client.java.query.Statement; @@ -26,7 +28,8 @@ import com.couchbase.client.java.query.dsl.Expression; import com.couchbase.client.java.query.dsl.path.FromPath; import com.couchbase.client.java.query.dsl.path.LimitPath; import com.couchbase.client.java.query.dsl.path.WherePath; - +import com.couchbase.client.java.query.dsl.path.MutateLimitPath; +import com.couchbase.client.java.query.dsl.path.DeleteUsePath; import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.repository.query.support.N1qlUtils; import org.springframework.data.domain.Pageable; @@ -71,30 +74,40 @@ public class PartTreeN1qlBasedQuery extends AbstractN1qlBasedQuery { String bucketName = getCouchbaseOperations().getCouchbaseBucket().name(); Expression bucket = N1qlUtils.escapedBucket(bucketName); - FromPath select; - if (partTree.isCountProjection()) { - select = select(count("*")); + if (partTree.isDelete()) { + DeleteUsePath deleteUsePath = deleteFrom(bucket); + N1qlMutateQueryCreator queryCreator = new N1qlMutateQueryCreator(partTree, accessor, deleteUsePath, getCouchbaseOperations().getConverter(), getQueryMethod()); + MutateLimitPath mutateFromWhereOrderBy = queryCreator.createQuery(); + if (partTree.isLimiting()) { + return mutateFromWhereOrderBy.limit(partTree.getMaxResults()); + } else { + return mutateFromWhereOrderBy.returning(createReturningExpressionForDelete(bucketName)); + } } else { + FromPath select; + if (partTree.isCountProjection()) { + select = select(count("*")); + } else { select = N1qlUtils.createSelectClauseForEntity(bucketName, returnedType, this.getCouchbaseOperations().getConverter()); - } - WherePath selectFrom = select.from(bucket); + } + WherePath selectFrom = select.from(bucket); + N1qlQueryCreator queryCreator = new N1qlQueryCreator(partTree, accessor, selectFrom, + getCouchbaseOperations().getConverter(), getQueryMethod()); + LimitPath selectFromWhereOrderBy = queryCreator.createQuery(); - N1qlQueryCreator queryCreator = new N1qlQueryCreator(partTree, accessor, selectFrom, - getCouchbaseOperations().getConverter(), getQueryMethod()); - LimitPath selectFromWhereOrderBy = queryCreator.createQuery(); - - if (queryMethod.isPageQuery()) { - Pageable pageable = accessor.getPageable(); - Assert.notNull(pageable, "Pageable must not be null!"); - return selectFromWhereOrderBy.limit(pageable.getPageSize()).offset(Math.toIntExact(pageable.getOffset())); - } else if (queryMethod.isSliceQuery() && accessor.getPageable().isPaged()) { - Pageable pageable = accessor.getPageable(); - Assert.notNull(pageable, "Pageable must not be null!"); - return selectFromWhereOrderBy.limit(pageable.getPageSize() + 1).offset(Math.toIntExact(pageable.getOffset())); - } else if (partTree.isLimiting()) { - return selectFromWhereOrderBy.limit(partTree.getMaxResults()); - } else { - return selectFromWhereOrderBy; + if (queryMethod.isPageQuery()) { + Pageable pageable = accessor.getPageable(); + Assert.notNull(pageable, "Pageable must not be null!"); + return selectFromWhereOrderBy.limit(pageable.getPageSize()).offset(Math.toIntExact(pageable.getOffset())); + } else if (queryMethod.isSliceQuery() && accessor.getPageable().isPaged()) { + Pageable pageable = accessor.getPageable(); + Assert.notNull(pageable, "Pageable must not be null!"); + return selectFromWhereOrderBy.limit(pageable.getPageSize() + 1).offset(Math.toIntExact(pageable.getOffset())); + } else if (partTree.isLimiting()) { + return selectFromWhereOrderBy.limit(partTree.getMaxResults()); + } else { + return selectFromWhereOrderBy; + } } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java index 1c158c53..bcb4a816 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java @@ -15,6 +15,8 @@ */ package org.springframework.data.couchbase.repository.query; +import static com.couchbase.client.java.query.Delete.deleteFrom; +import static com.couchbase.client.java.query.dsl.Expression.i; import static org.springframework.data.couchbase.core.support.TemplateUtils.*; import java.util.ArrayList; @@ -27,6 +29,7 @@ import com.couchbase.client.java.document.json.JsonArray; import com.couchbase.client.java.document.json.JsonObject; import com.couchbase.client.java.document.json.JsonValue; import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.repository.query.support.N1qlUtils; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.QueryMethod; @@ -71,6 +74,22 @@ public class StringBasedN1qlQueryParser { */ public static final String SPEL_FILTER = "#" + SPEL_PREFIX + ".filter"; + /** + * Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.query.Query @Query} + * annotation's inline statement. This will be replaced by the correct delete expression needed + * Eg. "#{{@value SPEL_DELETE}} WHERE test = true". + * Note this only makes sense once, as the beginning of the statement. + */ + public static final String SPEL_DELETE = "#" + SPEL_PREFIX + ".delete"; + + /** + * Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.query.Query @Query} + * annotation's inline statement. This will be replaced by the correct returning clause needed + * for entity mapping. Eg. "#{{@value SPEL_RETURNING}} WHERE test = true". + * Note this only makes sense once, as the beginning of the statement. + */ + public static final String SPEL_RETURNING = "#" + SPEL_PREFIX + ".returning"; + /** regexp that detect $named placeholder (starts with a letter, then alphanum chars) */ public static final Pattern NAMED_PLACEHOLDER_PATTERN = Pattern.compile("\\W(\\$\\p{Alpha}\\p{Alnum}*)\\b"); @@ -116,10 +135,12 @@ public class StringBasedN1qlQueryParser { } else { selectEntity = "SELECT " + entity + ", " + b + ".* FROM " + b; } - String typeSelection = "`" + typeField + "` = \"" + typeValue.getName() + "\""; - return new N1qlSpelValues(selectEntity, entity, b, typeSelection); + String delete = deleteFrom(i(bucketName)).toString(); + String returning = " returning " + N1qlUtils.createReturningExpressionForDelete(bucketName).toString(); + + return new N1qlSpelValues(selectEntity, entity, b, typeSelection, delete, returning); } //this static method can be used to test the parsing behavior for Couchbase specific spel variables @@ -265,17 +286,30 @@ public class StringBasedN1qlQueryParser { public final String bucket; /** - * #{{@value SPEL_FILTER}. + * #{{@value SPEL_FILTER}}. * filter will be replaced by an expression allowing to select only entries matching the entity in a WHERE clause. */ public final String filter; - public N1qlSpelValues(String selectClause, String entityFields, String bucket, String filter) { + /** + * #{{@value SPEL_DELETE}}. + * delete will be replaced by a delete expression. + */ + public final String delete; + + /** + * #{{@value SPEL_RETURNING}}. + * returning will be replaced by a returning expression allowing to return the entity and meta information on deletes. + */ + public final String returning; + + public N1qlSpelValues(String selectClause, String entityFields, String bucket, String filter, String delete, String returning) { this.selectEntity = selectClause; this.fields = entityFields; this.bucket = bucket; this.filter = filter; + this.delete = delete; + this.returning = returning; } } - } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java index 1bb92121..9cecde15 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java @@ -16,6 +16,13 @@ package org.springframework.data.couchbase.repository.query; +import static com.couchbase.client.java.query.Delete.deleteFrom; +import static com.couchbase.client.java.query.dsl.Expression.i; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import com.couchbase.client.java.document.json.JsonValue; import com.couchbase.client.java.query.N1qlQuery; diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlQueryCreatorUtils.java b/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlQueryCreatorUtils.java new file mode 100644 index 00000000..b6f196c5 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlQueryCreatorUtils.java @@ -0,0 +1,198 @@ +/* + * Copyright 2017 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.repository.query.support; + +import static com.couchbase.client.java.query.dsl.Expression.*; + +import java.util.Collection; +import java.util.Iterator; + +import com.couchbase.client.java.document.json.JsonArray; +import com.couchbase.client.java.query.dsl.Expression; +import com.couchbase.client.java.query.dsl.functions.PatternMatchingFunctions; +import com.couchbase.client.java.query.dsl.functions.StringFunctions; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.repository.query.ConvertingIterator; +import org.springframework.data.mapping.context.PersistentPropertyPath; +import org.springframework.data.repository.query.parser.Part; + +/** + * Utils for creating part tree expressions + * + * @author Subhashni Balakrishnan + */ +public class N1qlQueryCreatorUtils { + public static Expression prepareExpression(CouchbaseConverter converter, Part part, Iterator iterator) { + PersistentPropertyPath path = N1qlUtils.getPathWithAlternativeFieldNames( + converter, part.getProperty()); + ConvertingIterator parameterValues = new ConvertingIterator(iterator, converter); + + //get the whole doted path with fieldNames instead of potentially wrong propNames + String fieldNamePath = N1qlUtils.getDottedPathWithAlternativeFieldNames(path); + + //deal with ignore case + boolean ignoreCase = false; + Class leafType = converter.getWriteClassFor(path.getLeafProperty().getType()); + boolean isString = leafType == String.class; + if (part.shouldIgnoreCase() == Part.IgnoreCaseType.WHEN_POSSIBLE) { + ignoreCase = isString; + } else if (part.shouldIgnoreCase() == Part.IgnoreCaseType.ALWAYS) { + if (!isString) { + throw new IllegalArgumentException(String.format("Part %s must be of type String but was %s", fieldNamePath, leafType)); + } + ignoreCase = true; + } + + return createExpression(part.getType(), fieldNamePath, ignoreCase, parameterValues); + } + + + public static Expression createExpression(Part.Type partType, String fieldNamePath, boolean ignoreCase, Iterator parameterValues) { + //create the left hand side of the expression, taking ignoreCase into account + Expression left = ignoreCase ? StringFunctions.lower(x(fieldNamePath)) : x(fieldNamePath); + + switch (partType) { + case BETWEEN: + return left.between(leftAndRight(parameterValues, ignoreCase)); + case IS_NOT_NULL: + return left.isNotNull(); + case IS_NULL: + return left.isNull(); + case NEGATING_SIMPLE_PROPERTY: + return left.ne(right(parameterValues, ignoreCase)); + case SIMPLE_PROPERTY: + return left.eq(right(parameterValues, ignoreCase)); + case BEFORE: + case LESS_THAN: + return left.lt(right(parameterValues, ignoreCase)); + case LESS_THAN_EQUAL: + return left.lte(right(parameterValues, ignoreCase)); + case GREATER_THAN_EQUAL: + return left.gte(right(parameterValues, ignoreCase)); + case AFTER: + case GREATER_THAN: + return left.gt(right(parameterValues, ignoreCase)); + case NOT_LIKE: + return left.notLike(right(parameterValues, ignoreCase)); + case LIKE: + return left.like(right(parameterValues, ignoreCase)); + case STARTING_WITH: + return left.like(like(parameterValues, ignoreCase, false, true)); + case ENDING_WITH: + return left.like(like(parameterValues, ignoreCase, true, false)); + case NOT_CONTAINING: + return left.notLike(like(parameterValues, ignoreCase, true, true)); + case CONTAINING: + return left.like(like(parameterValues, ignoreCase, true, true)); + case NOT_IN: + return left.notIn(rightArray(parameterValues)); + case IN: + return left.in(rightArray(parameterValues)); + case TRUE: + return left.eq(true); + case FALSE: + return left.eq(false); + case REGEX: + return regexp(fieldNamePath, parameterValues); + case EXISTS: + return left.isNotMissing(); + case WITHIN: + case NEAR: + default: + throw new IllegalArgumentException("Unsupported keyword in N1QL query derivation"); + } + } + + + protected static Expression regexp(String left, Iterator parameterValues) { + Object next = parameterValues.next(); + + String pattern; + if (next == null) { + pattern = ""; + } else { + pattern = String.valueOf(next); + } + return PatternMatchingFunctions.regexpLike(left, pattern); + } + + protected static Expression leftAndRight(Iterator parameterValues, boolean ignoreCase) { + return right(parameterValues, ignoreCase).and(right(parameterValues, ignoreCase)); + } + + protected static Expression like(Iterator parameterValues, boolean ignoreCase, + boolean anyPrefix, boolean anySuffix) { + Object next = parameterValues.next(); + if (next == null) { + return Expression.NULL(); + } + + Expression converted; + if (next instanceof String) { + String pattern = (String) next; + if (anyPrefix) { + pattern = "%" + pattern; + } + if (anySuffix) { + pattern = pattern + "%"; + } + converted = s(pattern); + + } else { + converted = x(String.valueOf(next)); + } + + if (ignoreCase) { + return StringFunctions.lower(converted); + } + return converted; + } + + protected static Expression right(Iterator parameterValues, boolean ignoreCase) { + Object next = parameterValues.next(); + if (next == null) { + return Expression.NULL(); + } + + Expression converted; + if (next instanceof String) { + converted = s((String) next); + } else { + converted = x(String.valueOf(next)); + } + + if (ignoreCase) { + return StringFunctions.lower(converted); + } + return converted; + } + + protected static JsonArray rightArray(Iterator parameterValues) { + Object next = parameterValues.next(); + + Object[] values; + if (next instanceof Collection) { + values = ((Collection) next).toArray(); + } else if (next.getClass().isArray()) { + values = (Object[]) next; + } else { + values = new Object[] {next}; + } + return JsonArray.from(values); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlUtils.java b/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlUtils.java index 49f07f1d..7bc10fee 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlUtils.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlUtils.java @@ -123,6 +123,32 @@ public class N1qlUtils { return select(propertiesExp); } + /** + * Creates the returning clause for N1ql deletes with all attributes of the entity and meta information + * + * @param bucketName the bucket that stores the entity documents (will be escaped). + * @return the needed returning clause of the statement. + */ + public static Expression createReturningExpressionForDelete(String bucketName) { + Expression fullEntity = path(i(bucketName), "*"); + Expression metaId = path(meta(i(bucketName)), "id").as(SELECT_ID); + Expression metaCas = path(meta(i(bucketName)), "cas").as(SELECT_CAS); + List expList = new ArrayList(); + expList.add(fullEntity); + expList.add(metaId); + expList.add(metaCas); + + StringBuilder sb = new StringBuilder(); + for (Expression exp: expList) { + if (sb.length() != 0) { + sb.append(", "); + } + sb.append(exp.toString()); + } + + return x(sb.toString()); + } + /** * Produce a {@link Statement} that corresponds to the SELECT clause for looking for Spring Data entities * stored in Couchbase. Notably it will select the content of the document AND its id and cas. diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQueryTest.java b/src/test/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQueryTest.java index 80d64cef..a08e0663 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQueryTest.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQueryTest.java @@ -222,28 +222,6 @@ public class AbstractN1qlBasedQueryTest { verify(mock, never()).executeSingleProjection(any(N1qlQuery.class)); } - @Test - public void shouldThrowWhenModifyingType() throws Exception { - - Method method = SampleRepository.class.getMethod("modifyingMethod"); - CouchbaseQueryMethod queryMethod = spy(new CouchbaseQueryMethod(method, metadata, projectionFactory, context)); - when(queryMethod.isModifyingQuery()).thenReturn(true); - - N1qlQuery query = Mockito.mock(N1qlQuery.class); - Pageable pageable = Mockito.mock(Pageable.class); - AbstractN1qlBasedQuery mock = mock(AbstractN1qlBasedQuery.class); - when(mock.executeDependingOnType(any(N1qlQuery.class), any(N1qlQuery.class), any(QueryMethod.class), any(Pageable.class), - any(Class.class))).thenCallRealMethod(); - - try { mock.executeDependingOnType(query, query, queryMethod, pageable, Sample.class); fail(); } catch (UnsupportedOperationException e) { } - verify(mock, never()).executeCollection(any(N1qlQuery.class), any(Class.class)); - verify(mock, never()).executeEntity(any(N1qlQuery.class), any(Class.class)); - verify(mock, never()).executeStream(any(N1qlQuery.class), any(Class.class)); - verify(mock, never()).executePaged(any(N1qlQuery.class), any(N1qlQuery.class), any(Pageable.class), any(Class.class)); - verify(mock, never()).executeSliced(any(N1qlQuery.class), any(N1qlQuery.class), any(Pageable.class), any(Class.class)); - verify(mock, never()).executeSingleProjection(any(N1qlQuery.class)); - } - @Test public void shouldExecuteSingleProjectionWhenRandomObjectReturnType() throws Exception { diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTest.java b/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTest.java index 2877c330..3108bb8a 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTest.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTest.java @@ -1,13 +1,16 @@ package org.springframework.data.couchbase.repository.query; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.*; import static org.springframework.data.couchbase.repository.query.N1qlQueryCreator.*; import java.util.Arrays; import java.util.Iterator; import java.util.List; +import com.couchbase.client.java.query.dsl.Expression; import org.junit.Test; +import org.springframework.data.couchbase.repository.query.support.N1qlQueryCreatorUtils; import org.springframework.data.repository.query.parser.Part; import com.couchbase.client.java.query.dsl.Expression; @@ -24,9 +27,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field BETWEEN 1 AND 2"; String expectedIgnoreCase = "LOWER(doc.field) BETWEEN LOWER(\"c\") AND LOWER(\"d\")"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -41,9 +44,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field IS NOT NULL"; String expectedIgnoreCase = "LOWER(doc.field) IS NOT NULL"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -58,9 +61,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field IS NULL"; String expectedIgnoreCase = "LOWER(doc.field) IS NULL"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -75,9 +78,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field < 1"; String expectedIgnoreCase = "LOWER(doc.field) < LOWER(\"b\")"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -92,9 +95,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field <= 1"; String expectedIgnoreCase = "LOWER(doc.field) <= LOWER(\"b\")"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -109,9 +112,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field > 1"; String expectedIgnoreCase = "LOWER(doc.field) > LOWER(\"b\")"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -126,9 +129,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field >= 1"; String expectedIgnoreCase = "LOWER(doc.field) >= LOWER(\"b\")"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -143,9 +146,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field < 1"; String expectedIgnoreCase = "LOWER(doc.field) < LOWER(\"b\")"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -160,9 +163,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field > 1"; String expectedIgnoreCase = "LOWER(doc.field) > LOWER(\"b\")"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -177,9 +180,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field NOT LIKE 1"; String expectedIgnoreCase = "LOWER(doc.field) NOT LIKE LOWER(\"b\")"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -194,9 +197,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field LIKE 1"; String expectedIgnoreCase = "LOWER(doc.field) LIKE LOWER(\"b\")"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -211,9 +214,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field LIKE 1"; String expectedIgnoreCase = "LOWER(doc.field) LIKE LOWER(\"b%\")"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -228,9 +231,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field LIKE 1"; String expectedIgnoreCase = "LOWER(doc.field) LIKE LOWER(\"%b\")"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -245,9 +248,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field NOT LIKE 1"; String expectedIgnoreCase = "LOWER(doc.field) NOT LIKE LOWER(\"%b%\")"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -262,9 +265,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field LIKE 1"; String expectedIgnoreCase = "LOWER(doc.field) LIKE LOWER(\"%b%\")"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -279,9 +282,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field NOT IN [1]"; String expectedIgnoreCase = "LOWER(doc.field) NOT IN [\"b\"]"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -297,8 +300,8 @@ public class N1qlQueryCreatorTest { String expected = "doc.field NOT IN [\"av1\",\"av2\"]"; String expectedIgnoreCase = "LOWER(doc.field) NOT IN [\"bv1\",\"bv2\"]"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); @@ -313,8 +316,8 @@ public class N1qlQueryCreatorTest { String expected = "doc.field NOT IN [\"av1\",\"av2\"]"; String expectedIgnoreCase = "LOWER(doc.field) NOT IN [\"bv1\",\"bv2\"]"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); @@ -328,9 +331,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field IN [1]"; String expectedIgnoreCase = "LOWER(doc.field) IN [\"b\"]"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -346,8 +349,8 @@ public class N1qlQueryCreatorTest { String expected = "doc.field IN [\"av1\",\"av2\"]"; String expectedIgnoreCase = "LOWER(doc.field) IN [\"bv1\",\"bv2\"]"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); @@ -362,8 +365,8 @@ public class N1qlQueryCreatorTest { String expected = "doc.field IN [\"av1\",\"av2\"]"; String expectedIgnoreCase = "LOWER(doc.field) IN [\"bv1\",\"bv2\"]"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); @@ -374,7 +377,7 @@ public class N1qlQueryCreatorTest { Part.Type keyword = Part.Type.NEAR; Iterator values = Arrays.asList("a", 1, "b").iterator(); - Expression exp = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); } @Test(expected = IllegalArgumentException.class) @@ -382,7 +385,7 @@ public class N1qlQueryCreatorTest { Part.Type keyword = Part.Type.WITHIN; Iterator values = Arrays.asList("a", 1, "b").iterator(); - Expression exp = createExpression(keyword, "doc.field", false, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); } @Test @@ -393,9 +396,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "REGEXP_LIKE(doc.field, \"1\")"; String expectedIgnoreCase = "REGEXP_LIKE(doc.field, \"b\")"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -410,9 +413,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field IS NOT MISSING"; String expectedIgnoreCase = "LOWER(doc.field) IS NOT MISSING"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -427,9 +430,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field = TRUE"; String expectedIgnoreCase = "LOWER(doc.field) = TRUE"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -444,9 +447,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field = FALSE"; String expectedIgnoreCase = "LOWER(doc.field) = FALSE"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -461,9 +464,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field != 1"; String expectedIgnoreCase = "LOWER(doc.field) != LOWER(\"b\")"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); @@ -478,9 +481,9 @@ public class N1qlQueryCreatorTest { String expectedNum = "doc.field = 1"; String expectedIgnoreCase = "LOWER(doc.field) = LOWER(\"b\")"; - Expression exp = createExpression(keyword, "doc.field", false, values); - Expression expNum = createExpression(keyword, "doc.field", false, values); - Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + Expression exp = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expNum = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = N1qlQueryCreatorUtils.createExpression(keyword, "doc.field", true, values); assertEquals(expected, exp.toString()); assertEquals(expectedNum, expNum.toString()); diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1QlBasedQueryTest.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1QlBasedQueryTest.java index a532813b..ab6a9eb5 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1QlBasedQueryTest.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1QlBasedQueryTest.java @@ -63,4 +63,25 @@ public class StringN1QlBasedQueryTest { assertEquals("SELECT COUNT(*) AS " + CountFragment.COUNT_ALIAS + " FROM `B` WHERE true", parsed); } + + @Test + public void testDeletePlaceholder() throws Exception { + String statement = spel(SPEL_DELETE) + " WHERE test = 1 AND " + spel(SPEL_FILTER); + String parsed = new StringBasedN1qlQueryParser(statement, null, "B", "_class", String.class) + .doParse(SPEL_PARSER, SPEL_EVALUATION_CONTEXT, true); + + assertEquals("DELETE FROM `B` WHERE test = 1 AND `_class` = " + + "\"java.lang.String\"", parsed); + } + + @Test + public void testReturningPlaceholder() throws Exception { + String statement = spel(SPEL_DELETE) + " WHERE test = 1 AND " + spel(SPEL_FILTER) + spel(SPEL_RETURNING) ; + String parsed = new StringBasedN1qlQueryParser(statement, null, "B", "_class", String.class) + .doParse(SPEL_PARSER, SPEL_EVALUATION_CONTEXT, true); + + assertEquals("DELETE FROM `B` WHERE test = 1 AND `_class` = " + + "\"java.lang.String\" returning `B`.*, META(`B`).id AS _ID, META(`B`).cas AS _CAS", parsed); + } + } \ No newline at end of file