From 6149873f755ada57f0105c6ad882b37c2aa12841 Mon Sep 17 00:00:00 2001 From: Michael Reiche <48999328+mikereiche@users.noreply.github.com> Date: Thu, 23 Jun 2022 09:40:34 -0700 Subject: [PATCH] Add support for ignoreCase in queries derived from method names. Closes #984, #1481. --- .../document/CouchbaseDocumentSerializer.java | 16 +- .../convert/MappingCouchbaseConverter.java | 59 ++++--- .../couchbase/core/query/QueryCriteria.java | 161 +++++++++++++----- .../repository/query/N1qlQueryCreator.java | 53 ++++-- .../core/query/QueryCriteriaTests.java | 4 +- .../data/couchbase/domain/UserRepository.java | 2 + ...aseRepositoryQuerydslIntegrationTests.java | 12 +- .../query/N1qlQueryCreatorTests.java | 16 ++ 8 files changed, 220 insertions(+), 103 deletions(-) diff --git a/src/main/java/com/querydsl/couchbase/document/CouchbaseDocumentSerializer.java b/src/main/java/com/querydsl/couchbase/document/CouchbaseDocumentSerializer.java index c540ec03c..6058f6515 100644 --- a/src/main/java/com/querydsl/couchbase/document/CouchbaseDocumentSerializer.java +++ b/src/main/java/com/querydsl/couchbase/document/CouchbaseDocumentSerializer.java @@ -17,7 +17,6 @@ import java.util.Collection; import java.util.List; -import java.util.Locale; import java.util.regex.Pattern; import org.springframework.data.couchbase.core.query.QueryCriteria; @@ -48,8 +47,6 @@ */ public abstract class CouchbaseDocumentSerializer implements Visitor { - boolean workInProgress = true; - public Object handle(Expression expression) { return expression.accept(this, null); } @@ -167,25 +164,22 @@ public Object visit(Operation expr, Void context) { return QueryCriteria.where(asDBKey(expr, 0)).startingWith(asDBValue(expr, 1)); } else if (op == Ops.STARTS_WITH_IC) { // return asDocument(asDBKey(expr, 0), new CBRegularExpression("^" + regexValue(expr, 1), "i")); - return QueryCriteria.where(asDBKey(expr, 0)).upper() - .startingWith(asDBValue(expr, 1).toString().toUpperCase(Locale.ROOT)); + return QueryCriteria.where(asDBKey(expr, 0)).startingWith(true, asDBValue(expr, 1).toString()); } else if (op == Ops.ENDS_WITH) { // return asDocument(asDBKey(expr, 0), new CBRegularExpression(regexValue(expr, 1) + "$")); return QueryCriteria.where(asDBKey(expr, 0)).endingWith(asDBValue(expr, 1)); } else if (op == Ops.ENDS_WITH_IC) { // return asDocument(asDBKey(expr, 0), new CBRegularExpression(regexValue(expr, 1) + "$", "i")); - return QueryCriteria.where(asDBKey(expr, 0)).upper() - .endingWith(asDBValue(expr, 1).toString().toUpperCase(Locale.ROOT)); + return QueryCriteria.where(asDBKey(expr, 0)).endingWith(true, asDBValue(expr, 1).toString()); } else if (op == Ops.EQ_IGNORE_CASE) { // return asDocument(asDBKey(expr, 0), new CBRegularExpression("^" + regexValue(expr, 1) + "$", "i")); - return QueryCriteria.where(asDBKey(expr, 0)).upper().eq(asDBValue(expr, 1).toString().toUpperCase(Locale.ROOT)); + return QueryCriteria.where(asDBKey(expr, 0)).eq(true, asDBValue(expr, 1).toString()); } else if (op == Ops.STRING_CONTAINS) { // return asDocument(asDBKey(expr, 0), new CBRegularExpression(".*" + regexValue(expr, 1) + ".*")); return QueryCriteria.where(asDBKey(expr, 0)).containing(asDBValue(expr, 1)); } else if (op == Ops.STRING_CONTAINS_IC) { // return asDocument(asDBKey(expr, 0), new CBRegularExpression(".*" + regexValue(expr, 1) + ".*", "i")); - return QueryCriteria.where(asDBKey(expr, 0)).upper() - .containing(asDBValue(expr, 1).toString().toUpperCase(Locale.ROOT)); + return QueryCriteria.where(asDBKey(expr, 0)).containing(true, asDBValue(expr, 1).toString()); /* } else if (op == Ops.MATCHES) { //return asDocument(asDBKey(expr, 0), new CBRegularExpression(asDBValue(expr, 1).toString())); @@ -201,7 +195,7 @@ public Object visit(Operation expr, Void context) { } else if (op == Ops.LIKE_IC) { // String regex = ExpressionUtils.likeToRegex((Expression) expr.getArg(1)).toString(); // return asDocument(asDBKey(expr, 0), new CBRegularExpression(regex, "i")); - return QueryCriteria.where(asDBKey(expr, 0)).upper().like(asDBValue(expr, 1).toString().toUpperCase(Locale.ROOT)); + return QueryCriteria.where(asDBKey(expr, 0)).like(true, asDBValue(expr, 1).toString()); } else if (op == Ops.BETWEEN) { // Document value = new Document("$gte", this.asDBValue(expr, 1)); // value.append("$lte", this.asDBValue(expr, 2)); diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java index 8dff2e25f..e480f9c6d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java @@ -507,6 +507,39 @@ protected void writeInternal(final Object source, final CouchbaseDocument target target.setExpiration((int) (entity.getExpiryDuration().getSeconds())); + writeToTargetDocument(target, entity, accessor, idProperty, versionProperty, prefixes, suffixes, idAttributes); + + if (idProperty != null && target.getId() == null) { + String id = accessor.getProperty(idProperty, String.class); + if (idProperty.isAnnotationPresent(GeneratedValue.class) && (id == null || id.equals(""))) { + generatedValueInfo = idProperty.findAnnotation(GeneratedValue.class); + String generatedId = generateId(generatedValueInfo, prefixes, suffixes, idAttributes); + target.setId(generatedId); + // this is not effective if id is Immutable, and accessor.setProperty() returns a new object in getBean() + accessor.setProperty(idProperty, generatedId); + } else { + target.setId(id); + } + } + + entity.doWithAssociations(new AssociationHandler() { + @Override + public void doWithAssociation(final Association association) { + CouchbasePersistentProperty inverseProp = association.getInverse(); + Class type = inverseProp.getType(); + Object propertyObj = accessor.getProperty(inverseProp, type); + if (null != propertyObj) { + writePropertyInternal(propertyObj, target, inverseProp, false); + } + } + }); + + } + + private void writeToTargetDocument(final CouchbaseDocument target, final CouchbasePersistentEntity entity, + final ConvertingPropertyAccessor accessor, final CouchbasePersistentProperty idProperty, + final CouchbasePersistentProperty versionProperty, final TreeMap prefixes, + final TreeMap suffixes, final TreeMap idAttributes) { entity.doWithProperties(new PropertyHandler() { @Override public void doWithPersistentProperty(final CouchbasePersistentProperty prop) { @@ -550,32 +583,6 @@ public void doWithPersistentProperty(final CouchbasePersistentProperty prop) { } } }); - - if (idProperty != null && target.getId() == null) { - String id = accessor.getProperty(idProperty, String.class); - if (idProperty.isAnnotationPresent(GeneratedValue.class) && (id == null || id.equals(""))) { - generatedValueInfo = idProperty.findAnnotation(GeneratedValue.class); - String generatedId = generateId(generatedValueInfo, prefixes, suffixes, idAttributes); - target.setId(generatedId); - // this is not effective if id is Immutable, and accessor.setProperty() returns a new object in getBean() - accessor.setProperty(idProperty, generatedId); - } else { - target.setId(id); - } - } - - entity.doWithAssociations(new AssociationHandler() { - @Override - public void doWithAssociation(final Association association) { - CouchbasePersistentProperty inverseProp = association.getInverse(); - Class type = inverseProp.getType(); - Object propertyObj = accessor.getProperty(inverseProp, type); - if (null != propertyObj) { - writePropertyInternal(propertyObj, target, inverseProp, false); - } - } - }); - } /** 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 5c6795e59..190105e09 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 @@ -22,12 +22,13 @@ import java.util.Formatter; import java.util.LinkedList; import java.util.List; +import java.util.Locale; -import com.couchbase.client.core.error.CouchbaseException; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; +import com.couchbase.client.core.error.CouchbaseException; import com.couchbase.client.core.error.InvalidArgumentException; import com.couchbase.client.java.json.JsonArray; import com.couchbase.client.java.json.JsonObject; @@ -158,7 +159,7 @@ public QueryCriteria and(QueryCriteria criteria) { QueryCriteria qc = wrap(criteria); newThis.criteriaChain.add(qc); qc.setChainOperator(ChainOperator.AND); - newThis.chainOperator = ChainOperator.AND;// otherwise we get "A chain operator must be present when chaining" + newThis.chainOperator = ChainOperator.AND;// otherwise we get "A chain operator must be present when chaining" return newThis; } @@ -200,54 +201,92 @@ public QueryCriteria or(QueryCriteria criteria) { qc.criteriaChain = newThis.criteriaChain; newThis.criteriaChain.add(qc); qc.setChainOperator(ChainOperator.OR); - newThis.chainOperator = ChainOperator.OR;// otherwise we get "A chain operator must be present when chaining" + newThis.chainOperator = ChainOperator.OR;// otherwise we get "A chain operator must be present when chaining" return newThis; } public QueryCriteria eq(@Nullable Object o) { - return is(o); + return eq(false, o); + } + + public QueryCriteria eq(boolean ignoreCase, @Nullable Object o) { + return is(ignoreCase, o); } public QueryCriteria is(@Nullable Object o) { + return is(false, o); + }; + + public QueryCriteria is(boolean ignoreCase, @Nullable Object o) { operator = "="; - value = new Object[] { o }; + format = ignoreCase ? "lower(%1$s) %2$s %3$s" : "%1$s %2$s %3$s"; + value = new Object[] { ignoreCase ? o.toString().toLowerCase(Locale.ROOT) : o }; return this; } public QueryCriteria ne(@Nullable Object o) { + return ne(false, o); + } + + public QueryCriteria ne(boolean ignoreCase, @Nullable Object o) { operator = "!="; - value = new Object[] { o }; + format = ignoreCase ? "lower(%1$s) %2$s %3$s" : "%1$s %2$s %3$s"; + value = new Object[] { ignoreCase ? o.toString().toLowerCase(Locale.ROOT) : o }; return this; } public QueryCriteria lt(@Nullable Object o) { + return lt(false, o); + } + + public QueryCriteria lt(boolean ignoreCase, @Nullable Object o) { operator = "<"; - value = new Object[] { o }; + format = ignoreCase ? "lower(%1$s) %2$s %3$s" : "%1$s %2$s %3$s"; + value = new Object[] { ignoreCase ? o.toString().toLowerCase(Locale.ROOT) : o }; return this; } public QueryCriteria lte(@Nullable Object o) { + return lte(false, o); + } + + public QueryCriteria lte(boolean ignoreCase, @Nullable Object o) { operator = "<="; - value = new Object[] { o }; + format = ignoreCase ? "lower(%1$s) %2$s %3$s" : "%1$s %2$s %3$s"; + value = new Object[] { ignoreCase ? o.toString().toLowerCase(Locale.ROOT) : o }; return this; } public QueryCriteria gt(@Nullable Object o) { + return gt(false, o); + } + + public QueryCriteria gt(boolean ignoreCase, @Nullable Object o) { operator = ">"; - value = new Object[] { o }; + format = ignoreCase ? "lower(%1$s) %2$s %3$s" : "%1$s %2$s %3$s"; + value = new Object[] { ignoreCase ? o.toString().toLowerCase(Locale.ROOT) : o }; return this; } public QueryCriteria gte(@Nullable Object o) { + return gte(false, o); + } + + public QueryCriteria gte(boolean ignoreCase, @Nullable Object o) { operator = ">="; - value = new Object[] { o }; + format = ignoreCase ? "lower(%1$s) %2$s %3$s" : "%1$s %2$s %3$s"; + value = new Object[] { ignoreCase ? o.toString().toLowerCase(Locale.ROOT) : o }; return this; } public QueryCriteria startingWith(@Nullable Object o) { - operator = "STARTING_WITH"; - value = new Object[] { o }; - format = "%1$s like (%3$s||\"%%\")"; + return startingWith(false, o); + } + + public QueryCriteria startingWith(boolean ignoreCase, @Nullable Object o) { + operator = "like"; + value = new Object[] { ignoreCase ? o.toString().toLowerCase(Locale.ROOT) : o }; + format = ignoreCase ? "lower(%1$s) %2$s (%3$s||\"%%\")" : "%1$s %2$s (%3$s||\"%%\")"; return this; } @@ -259,9 +298,13 @@ public QueryCriteria plus(@Nullable Object o) { } public QueryCriteria endingWith(@Nullable Object o) { - operator = "ENDING_WITH"; - value = new Object[] { o }; - format = "%1$s like (\"%%\"||%3$s)"; + return endingWith(false, o); + } + + public QueryCriteria endingWith(boolean ignoreCase, @Nullable Object o) { + operator = "like"; + value = new Object[] { ignoreCase ? o.toString().toLowerCase(Locale.ROOT) : o }; + format = ignoreCase ? "lower(%1$s) %2$s (\"%%\"||%3$s)" : "%1$s %2$s (\"%%\"||%3$s)"; return this; } @@ -273,23 +316,38 @@ public QueryCriteria regex(@Nullable Object o) { } public QueryCriteria containing(@Nullable Object o) { - operator = "CONTAINS"; - value = new Object[] { o }; - format = "contains(%1$s, %3$s)"; + return containing(false, o); + } + + public QueryCriteria containing(boolean ignoreCase, @Nullable Object o) { + operator = "contains"; + value = new Object[] { ignoreCase ? o.toString().toLowerCase(Locale.ROOT) : o }; + format = ignoreCase ? "%2$s(lower(%1$s), %3$s)" : "%2$s(%1$s, %3$s)"; return this; } public QueryCriteria arrayContaining(@Nullable Object o) { - operator = "ARRAY_CONTAINING"; + return arrayContaining(false, o); + } + + public QueryCriteria arrayContaining(boolean ignoreCase, @Nullable Object o) { + operator = "array_containing"; + if (ignoreCase) { + throw new CouchbaseException("ignoreCase not supported in IN and NOT_IN"); + } value = new Object[] { o }; format = "array_containing(%1$s, %3$s)"; return this; } public QueryCriteria notContaining(@Nullable Object o) { - replaceThisAsWrapperOf(containing(o)); - operator = "NOT"; - format = "not( %3$s )"; + return notContaining(false, o); + } + + public QueryCriteria notContaining(boolean ignoreCase, @Nullable Object o) { + replaceThisAsWrapperOf(containing(ignoreCase, o)); + operator = "not"; + format = "%2$s %3$s"; return this; } @@ -297,8 +355,6 @@ public QueryCriteria negate() { replaceThisAsWrapperOf(this); operator = "NOT"; format = "not( %3$s )"; - // criteriaChain = new LinkedList<>(); - // criteriaChain.add(this); return this; } @@ -310,16 +366,23 @@ public QueryCriteria size() { } public QueryCriteria like(@Nullable Object o) { - operator = "LIKE"; - value = new Object[] { o }; - format = "%1$s like %3$s"; + return like(false, o); + } + + public QueryCriteria like(boolean ignoreCase, @Nullable Object o) { + operator = "like"; + value = new Object[] { ignoreCase ? o.toString().toLowerCase(Locale.ROOT) : o }; + format = ignoreCase ? "lower(%1$s) %2$s %3$s" : "%1$s %2$s %3$s"; return this; } public QueryCriteria notLike(@Nullable Object o) { - operator = "NOTLIKE"; - value = new Object[] { o }; - format = "not(%1$s like %3$s)"; + return notLike(false, o); + } + + public QueryCriteria notLike(boolean ignoreCase, @Nullable Object o) { + replaceThisAsWrapperOf(like(ignoreCase, o)); + this.negate(); return this; } @@ -365,23 +428,35 @@ public QueryCriteria isNotValued() { return this; } - public QueryCriteria within(@Nullable Object o) { - operator = "WITHIN"; - value = new Object[] { o }; - format = "%1$s within %3$s"; + public QueryCriteria within(boolean ignoreCase, @Nullable Object o) { + operator = "within"; + value = new Object[] { ignoreCase ? o.toString().toLowerCase(Locale.ROOT) : o }; + format = ignoreCase ? "lower(%1$s) %2$s %3$s" : "%1$s %2$s %3$s"; return this; } public QueryCriteria between(@Nullable Object o1, @Nullable Object o2) { + return between(false, o1, o2); + } + + public QueryCriteria between(boolean ignoreCase, @Nullable Object o1, @Nullable Object o2) { operator = "BETWEEN"; - value = new Object[] { o1, o2 }; - format = "%1$s between %3$s and %4$s"; + value = new Object[] { ignoreCase ? o1.toString().toLowerCase(Locale.ROOT) : o1, + ignoreCase ? o2.toString().toLowerCase(Locale.ROOT) : o2 }; + format = ignoreCase ? "lower(%1$s) between %3$s and %4$s" : "%1$s between %3$s and %4$s"; return this; } public QueryCriteria in(@Nullable Object... o) { - operator = "IN"; - format = "%1$s in %3$s"; + return in(false, o); + } + + public QueryCriteria in(boolean ignoreCase, @Nullable Object... o) { + operator = "in"; + if (ignoreCase) { + throw new CouchbaseException("ignoreCase not supported in IN and NOT_IN"); + } + format = "%1$s %2$s %3$s"; value = new Object[1]; if (o.length > 0) { if (o[0] instanceof JsonArray || o[0] instanceof List || o[0] instanceof Object[]) { @@ -405,7 +480,7 @@ public QueryCriteria in(@Nullable Object... o) { } else { // see QueryCriteriaTests.testNestedNotIn() - if arg to notIn is not cast to Object // notIn((Object) new String[] { "Alabama", "Florida" })); - throw new CouchbaseException("unhandled parameters "+o); + throw new CouchbaseException("unhandled parameters " + o); } } @@ -414,7 +489,11 @@ public QueryCriteria in(@Nullable Object... o) { } public QueryCriteria notIn(@Nullable Object... o) { - return in(o).negate(); + return in(false, o).negate(); + } + + public QueryCriteria notIn(boolean ignoreCase, @Nullable Object... o) { + return in(ignoreCase, o).negate(); } public QueryCriteria TRUE() { // true/false are reserved, use TRUE/FALSE 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 08313fc8d..406e6cd08 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 @@ -30,6 +30,7 @@ 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.couchbase.repository.query.support.N1qlUtils; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.PersistentEntity; @@ -126,7 +127,25 @@ protected Query complete(QueryCriteria criteria, Sort sort) { private QueryCriteria from(final Part part, final CouchbasePersistentProperty property, final QueryCriteria criteria, final Iterator parameters) { - + // deal with ignore case + PersistentPropertyPath path = N1qlUtils.getPathWithAlternativeFieldNames(converter, + part.getProperty()); + + // get the whole doted path with fieldNames instead of potentially wrong propNames + String fieldNamePath = N1qlUtils.getDottedPathWithAlternativeFieldNames(path); + + 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; + } final Part.Type type = part.getType(); /* NEAR(new String[]{"IsNear", "Near"}), @@ -134,32 +153,32 @@ private QueryCriteria from(final Part part, final CouchbasePersistentProperty pr switch (type) { case GREATER_THAN: case AFTER: - return criteria.gt(parameters.next()); + return criteria.gt(ignoreCase, parameters.next()); case GREATER_THAN_EQUAL: - return criteria.gte(parameters.next()); + return criteria.gte(ignoreCase, parameters.next()); case LESS_THAN: case BEFORE: - return criteria.lt(parameters.next()); + return criteria.lt(ignoreCase, parameters.next()); case LESS_THAN_EQUAL: - return criteria.lte(parameters.next()); + return criteria.lte(ignoreCase, parameters.next()); case SIMPLE_PROPERTY: - return criteria.eq(parameters.next()); + return criteria.eq(ignoreCase, parameters.next()); case NEGATING_SIMPLE_PROPERTY: - return criteria.ne(parameters.next()); + return criteria.ne(ignoreCase, parameters.next()); case CONTAINING: - return criteria.containing(parameters.next()); + return criteria.containing(ignoreCase, parameters.next()); case NOT_CONTAINING: - return criteria.notContaining(parameters.next()); + return criteria.notContaining(ignoreCase, parameters.next()); case STARTING_WITH: - return criteria.startingWith(parameters.next()); + return criteria.startingWith(ignoreCase, parameters.next()); case ENDING_WITH: - return criteria.endingWith(parameters.next()); + return criteria.endingWith(ignoreCase, parameters.next()); case LIKE: - return criteria.like(parameters.next()); + return criteria.like(ignoreCase, parameters.next()); case NOT_LIKE: - return criteria.notLike(parameters.next()); + return criteria.notLike(ignoreCase, parameters.next()); case WITHIN: - return criteria.within(parameters.next()); + return criteria.within(ignoreCase, parameters.next()); case IS_NULL: return criteria.isNull(/*parameters.next()*/); case IS_NOT_NULL: @@ -173,11 +192,11 @@ private QueryCriteria from(final Part part, final CouchbasePersistentProperty pr case REGEX: return criteria.regex(parameters.next()); case BETWEEN: - return criteria.between(parameters.next(), parameters.next()); + return criteria.between(ignoreCase, parameters.next(), parameters.next()); case IN: - return criteria.in(parameters.next()); + return criteria.in(ignoreCase, new Object[] { parameters.next() }); case NOT_IN: - return criteria.notIn(parameters.next()); + return criteria.notIn(ignoreCase, new Object[] { parameters.next() }); case TRUE: return criteria.TRUE(); case FALSE: 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 2d196ad13..0b701417b 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 @@ -174,7 +174,7 @@ void testContaining() { @Test void testNotContaining() { QueryCriteria c = where(i("name")).notContaining("Elvis"); - assertEquals("not( (contains(`name`, \"Elvis\")) )", c.export()); + assertEquals("not (contains(`name`, \"Elvis\"))", c.export()); } @Test @@ -192,7 +192,7 @@ void testLike() { @Test void testNotLike() { QueryCriteria c = where(i("name")).notLike("%Elvis%"); - assertEquals("not(`name` like \"%Elvis%\")", c.export()); + assertEquals("not( ( (`name` like \"%Elvis%\")) )", c.export()); } @Test 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 9eacb99f9..99a11fb5e 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java @@ -43,6 +43,8 @@ public interface UserRepository extends CouchbaseRepository { List findByFirstname(String firstname); + List findByFirstnameIgnoreCase(String firstname); + Stream findByLastname(String lastname); List findByFirstnameIn(String... firstnames); diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuerydslIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuerydslIntegrationTests.java index 9bfaeaadc..30cccf558 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuerydslIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuerydslIntegrationTests.java @@ -255,7 +255,7 @@ void testStartsWithIgnoreCase() { .filter(a -> a.getName().toUpperCase().startsWith(united.getName().toUpperCase().substring(0, 5))) .toArray(Airline[]::new)), "[unexpected] -> [missing]"); - assertEquals(" WHERE UPPER(name) like ($1||\"%\")", bq(predicate)); + assertEquals(" WHERE lower(name) like ($1||\"%\")", bq(predicate)); } } @@ -281,7 +281,7 @@ void testEndsWithIgnoreCase() { .filter(a -> a.getName().toUpperCase().endsWith(united.getName().toUpperCase().substring(1))) .toArray(Airline[]::new)), "[unexpected] -> [missing]"); - assertEquals(" WHERE UPPER(name) like (\"%\"||$1)", bq(predicate)); + assertEquals(" WHERE lower(name) like (\"%\"||$1)", bq(predicate)); } } @@ -293,7 +293,7 @@ void testEqIgnoreCase() { assertNull(comprises(result, Arrays.stream(saved).filter(a -> a.getName().equalsIgnoreCase(flyByNight.getName())).toArray(Airline[]::new)), "[unexpected] -> [missing]"); - assertEquals(" WHERE UPPER(name) = $1", bq(predicate)); + assertEquals(" WHERE lower(name) = $1", bq(predicate)); } { BooleanExpression predicate = airline.name.equalsIgnoreCase(united.getName()); @@ -302,7 +302,7 @@ void testEqIgnoreCase() { comprises(result, Arrays.stream(saved).filter(a -> a.getName().equalsIgnoreCase(united.getName())).toArray(Airline[]::new)), "[unexpected] -> [missing]"); - assertEquals(" WHERE UPPER(name) = $1", bq(predicate)); + assertEquals(" WHERE lower(name) = $1", bq(predicate)); } } @@ -329,7 +329,7 @@ void testContainsIgnoreCase() { .filter(a -> a.getName().toUpperCase(Locale.ROOT).contains("united".toUpperCase(Locale.ROOT))) .toArray(Airline[]::new)), "[unexpected] -> [missing]"); - assertEquals(" WHERE contains(UPPER(name), $1)", bq(predicate)); + assertEquals(" WHERE contains(lower(name), $1)", bq(predicate)); } } @@ -356,7 +356,7 @@ void testLikeIgnoreCase() { .filter(a -> a.getName().toUpperCase(Locale.ROOT).endsWith("Airlines".toUpperCase(Locale.ROOT))) .toArray(Airline[]::new)), "[unexpected] -> [missing]"); - assertEquals(" WHERE UPPER(name) like $1", bq(predicate)); + assertEquals(" WHERE lower(name) like $1", bq(predicate)); } } 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 5a610849e..dd980ed01 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 @@ -23,6 +23,7 @@ import java.lang.reflect.Method; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -81,6 +82,21 @@ void createsQueryCorrectly() throws Exception { assertEquals(query.export(), " WHERE " + where(i("firstname")).is("Oliver").export()); } + @Test + void createsQueryCorrectlyIgnoreCase() throws Exception { + String input = "findByFirstnameIgnoreCase"; + PartTree tree = new PartTree(input, User.class); + Method method = UserRepository.class.getMethod(input, String.class); + QueryMethod queryMethod = new QueryMethod(method, new DefaultRepositoryMetadata(UserRepository.class), + new SpelAwareProxyProjectionFactory()); + N1qlQueryCreator creator = new N1qlQueryCreator(tree, getAccessor(getParameters(method), "Oliver"), queryMethod, + converter, bucketName); + Query query = creator.createQuery(); + + assertEquals(query.export(), + " WHERE " + where("lower(" + i("firstname") + ")").is("Oliver".toLowerCase(Locale.ROOT)).export()); + } + @Test void createsQueryFieldAnnotationCorrectly() throws Exception { String input = "findByMiddlename";