Querydsl support. (#1330)

Closes #1288.
This commit is contained in:
Michael Reiche
2022-03-31 08:04:17 -07:00
committed by GitHub
parent 99b969833c
commit c288d053c9
32 changed files with 3264 additions and 145 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 the original author or authors.
* Copyright 2013-2022 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.
@@ -190,78 +190,78 @@ public class BasicCouchbasePersistentEntityTests {
@Configuration
static class Config {}
public static class SimpleDocument {}
class SimpleDocument {}
@Document(expiry = 10)
public static class SimpleDocumentWithExpiry {}
class SimpleDocumentWithExpiry {}
@Document(expiry = 10, touchOnRead = true)
public static class SimpleDocumentWithTouchOnRead {}
class SimpleDocumentWithTouchOnRead {}
/**
* Simple POJO to test default expiry.
*/
@Document
private class DefaultExpiry {}
class DefaultExpiry {}
/**
* Simple POJO to test default expiry unit.
*/
@Document(expiry = 78)
private class DefaultExpiryUnit {}
class DefaultExpiryUnit {}
/**
* Simple POJO to test limit expiry.
*/
@Document(expiry = 30, expiryUnit = TimeUnit.DAYS)
private class LimitDaysExpiry {}
class LimitDaysExpiry {}
/**
* Simple POJO to test larger than 30 days expiry.
*/
@Document(expiry = 31, expiryUnit = TimeUnit.DAYS)
public class OverLimitDaysExpiry {}
class OverLimitDaysExpiry {}
/**
* Simple POJO to test larger than 30 days expiry defined as an expression
*/
@Document(expiryExpression = "${document.expiry.larger.than.30days:31}", expiryUnit = TimeUnit.DAYS)
public class OverLimitDaysExpiryExpression {}
class OverLimitDaysExpiryExpression {}
/**
* Simple POJO to test larger than 30 days expiry, when expressed in default time unit (SECONDS).
*/
@Document(expiry = 31 * 24 * 60 * 60)
public class OverLimitSecondsExpiry {}
class OverLimitSecondsExpiry {}
/**
* Simple POJO to test constant expiry expression
*/
@Document(expiryExpression = "10")
private class ConstantExpiryExpression {}
class ConstantExpiryExpression {}
/**
* Simple POJO to test valid expiry expression by resolving simple property from environment
*/
@Document(expiryExpression = "${valid.document.expiry}")
private class ExpiryWithValidExpression {}
class ExpiryWithValidExpression {}
/**
* Simple POJO to test invalid expiry expression
*/
@Document(expiryExpression = "${invalid.document.expiry}")
private class ExpiryWithInvalidExpression {}
class ExpiryWithInvalidExpression {}
/**
* Simple POJO to test expiry expression logic failure to resolve property placeholder
*/
@Document(expiryExpression = "${missing.expiry}")
private class ExpiryWithMissingProperty {}
class ExpiryWithMissingProperty {}
/**
* Simple POJO to test that expiry and expiry expression cannot be used simultaneously
*/
@Document(expiry = 10, expiryExpression = "10")
private class ExpiryAndExpression {}
class ExpiryAndExpression {}
}

View File

@@ -32,6 +32,7 @@ import com.couchbase.client.java.json.JsonArray;
/**
* @author Mauro Monti
* @author Michael Reiche
*/
class QueryCriteriaTests {
@@ -69,7 +70,7 @@ class QueryCriteriaTests {
void testNestedAndCriteria() {
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(" (`name` = $1) and (`age` > $2 or `country` = $3)", c.export(new int[1], parameters, null));
assertEquals("[\"Bubba\",12,\"Austria\"]", parameters.toString());
}
@@ -77,19 +78,33 @@ class QueryCriteriaTests {
void testNestedOrCriteria() {
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(" (`name` = $1) or (`age` > $2 or `country` = $3)", c.export(new int[1], parameters, null));
assertEquals("[\"Bubba\",12,\"Austria\"]", parameters.toString());
}
@Test
void testNestedNotIn() {
QueryCriteria c = where(i("name")).is("Bubba").or(where(i("age")).gt(12).or(i("country")).is("Austria"))
.and(where(i("state")).notIn((Object) new String[] { "Alabama", "Florida" }));
QueryCriteria c = where(i("name")).is("Bubba").or(where(i("age")).gt(12).and(i("country")).is("Austria"))
.and(where(i("state")).notIn(new String[] { "Alabama", "Florida" }));
JsonArray parameters = JsonArray.create();
assertEquals("`name` = $1 or (`age` > $2 or `country` = $3) and (not( (`state` in $4) ))",
assertEquals(" ( (`name` = $1) or (`age` > $2 and `country` = $3)) and (not( (`state` in $4) ))",
c.export(new int[1], parameters, null));
}
@Test
void testNestedNotIn2() {
QueryCriteria c = where(i("name")).is("Bubba").or(where(i("age")).gt(12)).and(where(i("state")).eq("1"));
JsonArray parameters = JsonArray.create();
assertEquals(" ( (`name` = $1) or (`age` > $2)) and (`state` = $3)", c.export(new int[1], parameters, null));
}
@Test
void testNestedNotIn3() {
QueryCriteria c = where(i("name")).is("Bubba").or(where(i("age")).gt(12)).and(i("state")).eq("1");
JsonArray parameters = JsonArray.create();
assertEquals(" (`name` = $1) or (`age` > $2) and `state` = $3", c.export(new int[1], parameters, null));
}
@Test
void testLt() {
QueryCriteria c = where(i("name")).lt("Couch");
@@ -252,7 +267,7 @@ class QueryCriteriaTests {
@Test
void testFalse() {
QueryCriteria c = where(i("name")).FALSE();
assertEquals("not( (`name`) )", c.export());
assertEquals("not(`name`)", c.export());
}
@Test

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors
* Copyright 2012-2022 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.
@@ -24,15 +24,21 @@ import org.springframework.data.couchbase.core.mapping.Document;
@Document
@CompositeQueryIndex(fields = { "id", "name desc" })
@CompositeQueryIndex(fields = { "id.something", "name desc" })
/**
* @author Michael Reiche
*/
public class Airline extends ComparableEntity {
@Id String id;
@QueryIndexed String name;
String hqCountry;
@PersistenceConstructor
public Airline(String id, String name) {
public Airline(String id, String name, String hqCountry) {
this.id = id;
this.name = name;
this.hqCountry = hqCountry;
}
public String getId() {
@@ -43,4 +49,8 @@ public class Airline extends ComparableEntity {
return name;
}
public String getHqCountry() {
return hqCountry;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017-2019 the original author or authors.
* Copyright 2017-2022 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,17 +16,23 @@
package org.springframework.data.couchbase.domain;
import java.util.List;
import org.springframework.data.couchbase.repository.DynamicProxyable;
import org.springframework.data.couchbase.repository.Query;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* @author Michael Reiche
*/
@Repository
public interface AirlineRepository extends PagingAndSortingRepository<Airline, String> {
public interface AirlineRepository extends PagingAndSortingRepository<Airline, String>,
QuerydslPredicateExecutor<Airline>, DynamicProxyable<AirlineRepository> {
@Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and (name = $1)")
List<User> getByName(@Param("airline_name")String airlineName);
List<User> getByName(@Param("airline_name") String airlineName);
}

View File

@@ -115,7 +115,7 @@ public class CouchbaseRepositoryKeyValueIntegrationTests extends ClusterAwareInt
userRepository.delete(user);
// Airline does not have a version
Airline airline = new Airline(UUID.randomUUID().toString(), "MyAirline");
Airline airline = new Airline(UUID.randomUUID().toString(), "MyAirline", null);
// save the document - we don't care how on this call
airlineRepository.save(airline);
airlineRepository.save(airline); // If it was an insert it would fail. Can't tell if it is an upsert or replace.

View File

@@ -67,6 +67,7 @@ import org.springframework.data.couchbase.core.mapping.event.ValidatingCouchbase
import org.springframework.data.couchbase.core.query.N1QLExpression;
import org.springframework.data.couchbase.core.query.QueryCriteria;
import org.springframework.data.couchbase.domain.Address;
import org.springframework.data.couchbase.domain.AirlineRepository;
import org.springframework.data.couchbase.domain.Airport;
import org.springframework.data.couchbase.domain.AirportMini;
import org.springframework.data.couchbase.domain.AirportRepository;
@@ -124,6 +125,8 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
@Autowired AirportRepository airportRepository;
@Autowired AirlineRepository airlineRepository;
@Autowired UserRepository userRepository;
@Autowired UserSubmissionRepository userSubmissionRepository;

View File

@@ -76,7 +76,7 @@ public class ReactiveCouchbaseRepositoryKeyValueIntegrationTests extends Cluster
userRepository.delete(user);
// Airline does not have a version
Airline airline = new Airline(UUID.randomUUID().toString(), "MyAirline");
Airline airline = new Airline(UUID.randomUUID().toString(), "MyAirline", null);
// save the document - we don't care how on this call
airlineRepository.save(airline).block();
airlineRepository.save(airline).block(); // If it was an insert it would fail. Can't tell if an upsert or replace.

View File

@@ -0,0 +1,647 @@
/*
* Copyright 2017-2022 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
*
* https://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 static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.springframework.data.couchbase.util.Util.comprises;
import static org.springframework.data.couchbase.util.Util.exactly;
import java.time.Duration;
import java.util.Arrays;
import java.util.Locale;
import java.util.Optional;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.auditing.DateTimeProvider;
import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.mapping.event.ValidatingCouchbaseEventListener;
import org.springframework.data.couchbase.core.query.QueryCriteriaDefinition;
import org.springframework.data.couchbase.domain.Airline;
import org.springframework.data.couchbase.domain.AirlineRepository;
import org.springframework.data.couchbase.domain.NaiveAuditorAware;
import org.springframework.data.couchbase.domain.QAirline;
import org.springframework.data.couchbase.domain.time.AuditingDateTimeProvider;
import org.springframework.data.couchbase.repository.auditing.EnableCouchbaseAuditing;
import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories;
import org.springframework.data.couchbase.repository.support.BasicQuery;
import org.springframework.data.couchbase.repository.support.SpringDataCouchbaseSerializer;
import org.springframework.data.couchbase.util.Capabilities;
import org.springframework.data.couchbase.util.ClusterType;
import org.springframework.data.couchbase.util.IgnoreWhen;
import org.springframework.data.couchbase.util.JavaIntegrationTests;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import com.couchbase.client.java.env.ClusterEnvironment;
import com.couchbase.client.java.query.QueryScanConsistency;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.BooleanExpression;
/**
* Repository tests
*
* @author Michael Reiche
*/
@SpringJUnitConfig(CouchbaseRepositoryQuerydslIntegrationTests.Config.class)
@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED)
public class CouchbaseRepositoryQuerydslIntegrationTests extends JavaIntegrationTests {
@Autowired AirlineRepository airlineRepository;
static QAirline airline = QAirline.airline;
// saved
static Airline united = new Airline("1", "United Airlines", "US");
static Airline lufthansa = new Airline("2", "Lufthansa", "DE");
static Airline emptyStringAirline = new Airline("3", "Empty String", "");
static Airline nullStringAirline = new Airline("4", "Null String", null);
static Airline unitedLowercase = new Airline("5", "united airlines", "US");
static Airline[] saved = new Airline[] { united, lufthansa, emptyStringAirline, nullStringAirline, unitedLowercase };
// not saved
static Airline flyByNight = new Airline("1001", "Fly By Night", "UK");
static Airline sleepByDay = new Airline("1002", "Sleep By Day", "CA");
static Airline[] notSaved = new Airline[] { flyByNight, sleepByDay };
SpringDataCouchbaseSerializer serializer = new SpringDataCouchbaseSerializer(couchbaseTemplate.getConverter());
@BeforeAll
static public void beforeAll() {
callSuperBeforeAll(new Object() {});
ApplicationContext ac = new AnnotationConfigApplicationContext(
CouchbaseRepositoryQuerydslIntegrationTests.Config.class);
CouchbaseTemplate template = (CouchbaseTemplate) ac.getBean("couchbaseTemplate");
for (Airline airline : saved) {
template.insertById(Airline.class).one(airline);
}
template.findByQuery(Airline.class).withConsistency(REQUEST_PLUS).all();
}
@AfterAll
static public void afterAll() {
ApplicationContext ac = new AnnotationConfigApplicationContext(
CouchbaseRepositoryQuerydslIntegrationTests.Config.class);
CouchbaseTemplate template = (CouchbaseTemplate) ac.getBean("couchbaseTemplate");
for (Airline airline : saved) {
template.removeById(Airline.class).one(airline.getId());
}
template.findByQuery(Airline.class).withConsistency(REQUEST_PLUS).all();
callSuperAfterAll(new Object() {});
}
@Test
void testEq() {
{
BooleanExpression predicate = airline.name.eq(flyByNight.getName());
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(
comprises(result,
Arrays.stream(saved).filter(a -> a.getName().equals(flyByNight.getName())).toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE name = $1", bq(predicate));
}
{
BooleanExpression predicate = airline.name.eq(united.getName());
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(
comprises(result,
Arrays.stream(saved).filter(a -> a.getName().equals(united.getName())).toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE name = $1", bq(predicate));
}
}
// this gives hqCountry == "" and hqCountry is missing
// @Test
void testStringIsEmpty() {
{
BooleanExpression predicate = airline.hqCountry.isEmpty();
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(comprises(result, emptyStringAirline, nullStringAirline), "[unexpected] -> [missing]");
assertEquals(" WHERE UPPER(name) like $1", bq(predicate));
}
}
@Test
void testNot() {
{
BooleanExpression predicate = airline.name.eq(united.getName()).and(airline.hqCountry.eq(united.getHqCountry()))
.not();
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(comprises(result,
Arrays.stream(saved)
.filter(a -> !(a.getName().equals(united.getName()) && a.getHqCountry().equals(united.getHqCountry())))
.toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE not( ( (hqCountry = $1) and (name = $2)) )", bq(predicate));
}
{
BooleanExpression predicate = airline.name.in(Arrays.asList(united.getName())).not();
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(
comprises(result,
Arrays.stream(saved).filter(a -> !(a.getName().equals(united.getName()))).toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE not( (name = $1) )", bq(predicate));
}
{
BooleanExpression predicate = airline.name.eq(united.getName()).not();
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(
comprises(result,
Arrays.stream(saved).filter(a -> !(a.getName().equals(united.getName()))).toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE not( (name = $1) )", bq(predicate));
}
}
@Test
void testAnd() {
{
BooleanExpression predicate = airline.name.eq(united.getName()).and(airline.hqCountry.eq(united.getHqCountry()));
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(comprises(result,
Arrays.stream(saved)
.filter(a -> a.getName().equals(united.getName()) && a.getHqCountry().equals(united.getHqCountry()))
.toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE (name = $1) and (hqCountry = $2)", bq(predicate));
}
{
BooleanExpression predicate = airline.name.eq(united.getName())
.and(airline.hqCountry.eq(lufthansa.getHqCountry()));
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(comprises(result,
Arrays.stream(saved)
.filter(a -> a.getName().equals(united.getName()) && a.getHqCountry().equals(lufthansa.getHqCountry()))
.toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE (name = $1) and (hqCountry = $2)", bq(predicate));
}
}
@Test
void testOr() {
{
BooleanExpression predicate = airline.name.eq(united.getName())
.or(airline.hqCountry.eq(lufthansa.getHqCountry()));
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(comprises(result,
Arrays.stream(saved)
.filter(a -> a.getName().equals(united.getName()) || a.getName().equals(lufthansa.getName()))
.toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE (name = $1) or (hqCountry = $2)", bq(predicate));
}
}
@Test
void testNe() {
{
BooleanExpression predicate = airline.name.ne(united.getName());
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(
comprises(result,
Arrays.stream(saved).filter(a -> !a.getName().equals(united.getName())).toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE name != $1", bq(predicate));
}
}
@Test
void testStartsWith() {
{
BooleanExpression predicate = airline.name.startsWith(united.getName().substring(0, 5));
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(comprises(result, Arrays.stream(saved)
.filter(a -> a.getName().startsWith(united.getName().substring(0, 5))).toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE name like ($1||\"%\")", bq(predicate));
}
}
@Test
void testStartsWithIgnoreCase() {
{
BooleanExpression predicate = airline.name.startsWithIgnoreCase(united.getName().substring(0, 5));
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(comprises(result,
Arrays.stream(saved)
.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));
}
}
@Test
void testEndsWith() {
{
BooleanExpression predicate = airline.name.endsWith(united.getName().substring(1));
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(comprises(result, Arrays.stream(saved).filter(a -> a.getName().endsWith(united.getName().substring(1)))
.toArray(Airline[]::new)), "[unexpected] -> [missing]");
assertEquals(" WHERE name like (\"%\"||$1)", bq(predicate));
}
}
@Test
void testEndsWithIgnoreCase() {
{
BooleanExpression predicate = airline.name.endsWithIgnoreCase(united.getName().substring(1));
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(comprises(result,
Arrays.stream(saved)
.filter(a -> a.getName().toUpperCase().endsWith(united.getName().toUpperCase().substring(1)))
.toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE UPPER(name) like (\"%\"||$1)", bq(predicate));
}
}
@Test
void testEqIgnoreCase() {
{
BooleanExpression predicate = airline.name.equalsIgnoreCase(flyByNight.getName());
Iterable<Airline> result = airlineRepository.findAll(predicate);
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));
}
{
BooleanExpression predicate = airline.name.equalsIgnoreCase(united.getName());
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(
comprises(result,
Arrays.stream(saved).filter(a -> a.getName().equalsIgnoreCase(united.getName())).toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE UPPER(name) = $1", bq(predicate));
}
}
@Test
void testContains() {
{
BooleanExpression predicate = airline.name.contains("United");
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(
comprises(result, Arrays.stream(saved).filter(a -> a.getName().contains("United")).toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE contains(name, $1)", bq(predicate));
}
}
@Test
void testContainsIgnoreCase() {
{
BooleanExpression predicate = airline.name.containsIgnoreCase("united");
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(comprises(result,
Arrays.stream(saved)
.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));
}
}
@Test
void testLike() {
{
BooleanExpression predicate = airline.name.like("%nited%");
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(
comprises(result, Arrays.stream(saved).filter(a -> a.getName().contains("nited")).toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE name like $1", bq(predicate));
}
}
@Test
void testLikeIgnoreCase() {
{
BooleanExpression predicate = airline.name.likeIgnoreCase("%Airlines");
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(comprises(result,
Arrays.stream(saved)
.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));
}
}
// This is 'between' is inclusive
@Test
void testBetween() {
{
BooleanExpression predicate = airline.name.between(flyByNight.getName(), united.getName());
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(comprises(result,
Arrays.stream(saved)
.filter(
a -> a.getName().compareTo(flyByNight.getName()) >= 0 && a.getName().compareTo(united.getName()) <= 0)
.toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE name between $1 and $2", bq(predicate));
}
}
@Test
void testIn() {
{
BooleanExpression predicate = airline.name.in(Arrays.asList(united.getName()));
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(
comprises(result,
Arrays.stream(saved).filter(a -> a.getName().equals(united.getName())).toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE name = $1", bq(predicate));
}
{
BooleanExpression predicate = airline.name.in(Arrays.asList(united.getName(), lufthansa.getName()));
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(comprises(result,
Arrays.stream(saved)
.filter(a -> a.getName().equals(united.getName()) || a.getName().equals(lufthansa.getName()))
.toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE name in $1", bq(predicate));
}
{
BooleanExpression predicate = airline.name.in("Fly By Night", "Sleep By Day");
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(comprises(result,
Arrays.stream(saved)
.filter(a -> a.getName().equals(flyByNight.getName()) || a.getName().equals(sleepByDay.getName()))
.toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE name in $1", bq(predicate));
}
}
@Test
void testNotIn() {
{
BooleanExpression predicate = airline.name.notIn("Fly By Night", "Sleep By Day");
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(comprises(result,
Arrays.stream(saved)
.filter(a -> !(a.getName().equals(flyByNight.getName()) || a.getName().equals(sleepByDay.getName())))
.toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE not( (name in $1) )", bq(predicate));
}
{
BooleanExpression predicate = airline.name.notIn(united.getName());
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(
comprises(result,
Arrays.stream(saved).filter(a -> !a.getName().equals(united.getName())).toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE name != $1", bq(predicate));
}
}
@Test
@Disabled
void testColIsEmpty() {}
@Test
void testLt() {
{
BooleanExpression predicate = airline.name.lt(lufthansa.getName());
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(
comprises(result,
Arrays.stream(saved).filter(a -> a.getName().compareTo(lufthansa.getName()) < 0).toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE name < $1", bq(predicate));
}
}
@Test
void testGt() {
{
BooleanExpression predicate = airline.name.gt(lufthansa.getName());
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(
comprises(result,
Arrays.stream(saved).filter(a -> a.getName().compareTo(lufthansa.getName()) > 0).toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE name > $1", bq(predicate));
}
}
@Test
void testLoe() {
{
BooleanExpression predicate = airline.name.loe(lufthansa.getName());
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(comprises(result,
Arrays.stream(saved).filter(a -> a.getName().compareTo(lufthansa.getName()) <= 0).toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE name <= $1", bq(predicate));
}
}
@Test
void testGoe() {
{
BooleanExpression predicate = airline.name.goe(lufthansa.getName());
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(comprises(result,
Arrays.stream(saved).filter(a -> a.getName().compareTo(lufthansa.getName()) >= 0).toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE name >= $1", bq(predicate));
}
}
// when hqCountry == null, no value is stored therefore isNull is false. Only hqCountry:null gives isNull
// and we don't have that. Conversely, only hqCountry has a value (which is not 'null') gives isNotNull
// so isNull and isNotNull are *not* compliments
@Test
@Disabled
void testIsNull() {
{
BooleanExpression predicate = airline.hqCountry.isNull();
Optional<Airline> result = airlineRepository.findOne(predicate);
assertNull(exactly(result, nullStringAirline), "[unexpected] -> [missing]");
assertEquals(" WHERE name = $1", bq(predicate));
}
}
// when hqCountry == null, no value is stored therefore isNull is false. Only hqCountry:null gives isNull
// and we don't have that. Conversely, only hqCountry has a value (which is not 'null') gives isNotNull
// so isNull and isNotNull are *not* compliments
@Test
void testIsNotNull() {
{
BooleanExpression predicate = airline.hqCountry.isNotNull();
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(comprises(result, Arrays.stream(saved).filter(a -> a.getHqCountry() != null).toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE hqCountry is not null", bq(predicate));
}
}
@Test
@Disabled
void testContainsKey() {}
@Test
void testStringLength() {
{
BooleanExpression predicate = airline.name.length().eq(united.getName().length());
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(comprises(result,
Arrays.stream(saved).filter(a -> a.getName().length() == united.getName().length()).toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE LENGTH(name) = $1", bq(predicate));
}
{
BooleanExpression predicate = airline.name.length().eq(flyByNight.getName().length());
Iterable<Airline> result = airlineRepository.findAll(predicate);
assertNull(comprises(result, Arrays.stream(saved)
.filter(a -> a.getName().length() == flyByNight.getName().length()).toArray(Airline[]::new)),
"[unexpected] -> [missing]");
assertEquals(" WHERE LENGTH(name) = $1", bq(predicate));
}
}
private void sleep(int millis) {
try {
Thread.sleep(millis); // so they are executed out-of-order
} catch (InterruptedException ie) {
;
}
}
@Configuration
@EnableCouchbaseRepositories("org.springframework.data.couchbase")
@EnableCouchbaseAuditing(dateTimeProviderRef = "dateTimeProviderRef")
static class Config extends AbstractCouchbaseConfiguration {
@Override
public String getConnectionString() {
return connectionString();
}
@Override
public String getUserName() {
return config().adminUsername();
}
@Override
public String getPassword() {
return config().adminPassword();
}
@Override
public String getBucketName() {
return bucketName();
}
@Bean(name = "auditorAwareRef")
public NaiveAuditorAware testAuditorAware() {
return new NaiveAuditorAware();
}
@Override
public void configureEnvironment(final ClusterEnvironment.Builder builder) {
builder.ioConfig().maxHttpConnections(11).idleHttpConnectionTimeout(Duration.ofSeconds(4));
return;
}
@Bean(name = "dateTimeProviderRef")
public DateTimeProvider testDateTimeProvider() {
return new AuditingDateTimeProvider();
}
@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
@Bean
public ValidatingCouchbaseEventListener validationEventListener() {
return new ValidatingCouchbaseEventListener(validator());
}
}
String bq(Predicate predicate) {
BasicQuery basicQuery = new BasicQuery((QueryCriteriaDefinition) serializer.handle(predicate), null);
return basicQuery.export(new int[1]);
}
@Configuration
@EnableCouchbaseRepositories("org.springframework.data.couchbase")
@EnableCouchbaseAuditing(auditorAwareRef = "auditorAwareRef", dateTimeProviderRef = "dateTimeProviderRef")
static class ConfigRequestPlus extends AbstractCouchbaseConfiguration {
@Override
public String getConnectionString() {
return connectionString();
}
@Override
public String getUserName() {
return config().adminUsername();
}
@Override
public String getPassword() {
return config().adminPassword();
}
@Override
public String getBucketName() {
return bucketName();
}
@Bean(name = "auditorAwareRef")
public NaiveAuditorAware testAuditorAware() {
return new NaiveAuditorAware();
}
@Bean(name = "dateTimeProviderRef")
public DateTimeProvider testDateTimeProvider() {
return new AuditingDateTimeProvider();
}
@Override
public QueryScanConsistency getDefaultConsistency() {
return REQUEST_PLUS;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017-2019 the original author or authors.
* Copyright 2017-2022 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,7 +23,6 @@ import java.util.Optional;
import java.util.Properties;
import java.util.UUID;
import com.couchbase.client.java.query.QueryScanConsistency;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
@@ -58,6 +57,8 @@ import org.springframework.data.repository.query.QueryMethodEvaluationContextPro
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import com.couchbase.client.java.query.QueryScanConsistency;
/**
* @author Michael Nitschinger
* @author Michael Reiche
@@ -82,7 +83,7 @@ class StringN1qlQueryCreatorTests extends ClusterAwareIntegrationTests {
@Test
@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED)
void findUsingStringNq1l() throws Exception {
Airline airline = new Airline(UUID.randomUUID().toString(), "Continental");
Airline airline = new Airline(UUID.randomUUID().toString(), "Continental", "USA");
try {
Airline modified = couchbaseTemplate.upsertById(Airline.class).one(airline);
@@ -99,8 +100,8 @@ class StringN1qlQueryCreatorTests extends ClusterAwareIntegrationTests {
Query query = creator.createQuery();
ExecutableFindByQuery q = (ExecutableFindByQuery) couchbaseTemplate
.findByQuery(Airline.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).matching(query);
ExecutableFindByQuery q = (ExecutableFindByQuery) couchbaseTemplate.findByQuery(Airline.class)
.withConsistency(QueryScanConsistency.REQUEST_PLUS).matching(query);
Optional<Airline> al = q.one();
assertEquals(airline.toString(), al.get().toString());

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020 the original author or authors
* Copyright 2020-2022 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,84 +16,134 @@
package org.springframework.data.couchbase.util;
import java.io.InputStream;
import java.time.Duration;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.awaitility.Awaitility.with;
import java.io.InputStream;
import java.time.Duration;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
import org.springframework.data.util.Pair;
/**
* Provides a bunch of utility APIs that help with testing.
*
* @author Michael Reiche
*/
public class Util {
/**
* Waits and sleeps for a little bit of time until the given condition is met.
*
* <p>Sleeps 1ms between "false" invocations. It will wait at most one minute to prevent hanging forever in case
* the condition never becomes true.
*
* @param supplier return true once it should stop waiting.
*/
public static void waitUntilCondition(final BooleanSupplier supplier) {
waitUntilCondition(supplier, Duration.ofMinutes(1));
}
/**
* Waits and sleeps for a little bit of time until the given condition is met.
* <p>
* Sleeps 1ms between "false" invocations. It will wait at most one minute to prevent hanging forever in case the
* condition never becomes true.
*
* @param supplier return true once it should stop waiting.
*/
public static void waitUntilCondition(final BooleanSupplier supplier) {
waitUntilCondition(supplier, Duration.ofMinutes(1));
}
public static void waitUntilCondition(final BooleanSupplier supplier, Duration atMost) {
with().pollInterval(Duration.ofMillis(1)).await().atMost(atMost).until(supplier::getAsBoolean);
}
public static void waitUntilCondition(final BooleanSupplier supplier, Duration atMost) {
with().pollInterval(Duration.ofMillis(1)).await().atMost(atMost).until(supplier::getAsBoolean);
}
public static void waitUntilCondition(final BooleanSupplier supplier, Duration atMost, Duration delay) {
with().pollInterval(delay).await().atMost(atMost).until(supplier::getAsBoolean);
}
public static void waitUntilCondition(final BooleanSupplier supplier, Duration atMost, Duration delay) {
with().pollInterval(delay).await().atMost(atMost).until(supplier::getAsBoolean);
}
public static void waitUntilThrows(final Class<? extends Exception> clazz, final Supplier<Object> supplier) {
with()
.pollInterval(Duration.ofMillis(1))
.await()
.atMost(Duration.ofMinutes(1))
.until(() -> {
try {
supplier.get();
} catch (final Exception ex) {
return ex.getClass().isAssignableFrom(clazz);
}
return false;
});
}
public static void waitUntilThrows(final Class<? extends Exception> clazz, final Supplier<Object> supplier) {
with().pollInterval(Duration.ofMillis(1)).await().atMost(Duration.ofMinutes(1)).until(() -> {
try {
supplier.get();
} catch (final Exception ex) {
return ex.getClass().isAssignableFrom(clazz);
}
return false;
});
}
/**
* Returns true if a thread with the given name is currently running.
*
* @param name the name of the thread.
* @return true if running, false otherwise.
*/
public static boolean threadRunning(final String name) {
for (Thread t : Thread.getAllStackTraces().keySet()) {
if (t.getName().equalsIgnoreCase(name)) {
return true;
}
}
return false;
}
/**
* Returns true if a thread with the given name is currently running.
*
* @param name the name of the thread.
* @return true if running, false otherwise.
*/
public static boolean threadRunning(final String name) {
for (Thread t : Thread.getAllStackTraces().keySet()) {
if (t.getName().equalsIgnoreCase(name)) {
return true;
}
}
return false;
}
/**
* Reads a file from the resources folder (in the same path as the requesting test class).
*
* <p>The class will be automatically loaded relative to the namespace and converted
* to a string.
*
* @param filename the filename of the resource.
* @param clazz the reference class.
* @return the loaded string.
*/
public static String readResource(final String filename, final Class<?> clazz) {
String path = "/" + clazz.getPackage().getName().replace(".", "/") + "/" + filename;
InputStream stream = clazz.getResourceAsStream(path);
java.util.Scanner s = new java.util.Scanner(stream, UTF_8.name()).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
/**
* Reads a file from the resources folder (in the same path as the requesting test class).
* <p>
* The class will be automatically loaded relative to the namespace and converted to a string.
* </p>
*
* @param filename the filename of the resource.
* @param clazz the reference class.
* @return the loaded string.
*/
public static String readResource(final String filename, final Class<?> clazz) {
String path = "/" + clazz.getPackage().getName().replace(".", "/") + "/" + filename;
InputStream stream = clazz.getResourceAsStream(path);
java.util.Scanner s = new java.util.Scanner(stream, UTF_8.name()).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
public static <T> Pair<List<T>, List<T>> comprises(Iterable<T> source, T... airlines) {
List<T> unexpected = new LinkedList<>();
List<T> missing = new LinkedList<T>();
source.forEach(unexpected::add);
for (T t : airlines) {
if (!unexpected.contains(t)) {
missing.add(t);
} else {
unexpected.remove(t);
}
}
if (unexpected.isEmpty() && missing.isEmpty()) {
return null;
} else {
return Pair.of(unexpected, missing);
}
}
public static <T> Pair<List<T>, List<T>> exactly(Optional<T> result, T... airlines) {
List<T> source = new LinkedList<>();
if (result.isPresent()) {
source.add(result.get());
}
return comprises(source, airlines);
}
// should return null if items in source match (allAirlines - notAirlines)
public static <T> Pair<List<T>, List<T>> comprisesNot(Iterable<T> source, T[] allAirlines, T... notAirlines) {
List<T> expected = new LinkedList<>(Arrays.asList(allAirlines));
expected.removeAll(Arrays.asList(notAirlines));
List<T> unexpected = new LinkedList<>();
List<T> missing = new LinkedList<T>();
source.forEach(unexpected::add);// initially, everything returned is unexpected
for (T t : expected) {
if (!unexpected.contains(t)) {
missing.add(t); // if not returned, then it is missing
} else {
unexpected.remove(t); // if returned, then remove from unexpected
}
}
if (unexpected.isEmpty() && missing.isEmpty()) {
return null;
} else {
return Pair.of(unexpected, missing);
}
}
}