QuerydslKeyValuePredicateExecutorUnitTests.java

/*
 * Copyright 2014-2025 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.map;

import static org.assertj.core.api.Assertions.*;

import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.keyvalue.Person;
import org.springframework.data.keyvalue.QPerson;
import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory;
import org.springframework.data.map.QuerydslKeyValuePredicateExecutorUnitTests.QPersonRepository;
import org.springframework.data.querydsl.QSort;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.query.FluentQuery;
import org.springframework.data.util.Streamable;

/**
 * Unit tests for {@link org.springframework.data.keyvalue.repository.support.QuerydslKeyValuePredicateExecutor}.
 *
 * @author Christoph Strobl
 * @author Oliver Gierke
 * @author Thomas Darimont
 * @author Mark Paluch
 */
class QuerydslKeyValuePredicateExecutorUnitTests extends AbstractRepositoryUnitTests<QPersonRepository> {

	@BeforeEach
	void setUp() {
		repository.saveAll(LENNISTERS);
	}

	@Test // DATACMNS-525
	void findOneIsExecutedCorrectly() {

		Optional<Person> result = repository.findOne(QPerson.person.firstname.eq(CERSEI.getFirstname()));
		assertThat(result).hasValue(CERSEI);
	}

	@Test // DATACMNS-525
	void findAllIsExecutedCorrectly() {

		Iterable<Person> result = repository.findAll(QPerson.person.age.eq(CERSEI.getAge()));
		assertThat(result).contains(CERSEI, JAIME);
	}

	@Test // DATACMNS-525
	void findWithPaginationWorksCorrectly() {

		Page<Person> page1 = repository.findAll(QPerson.person.age.eq(CERSEI.getAge()), PageRequest.of(0, 1));

		assertThat(page1.getTotalElements()).isEqualTo(2L);
		assertThat(page1.getContent()).hasSize(1);
		assertThat(page1.hasNext()).isTrue();

		Page<Person> page2 = repository.findAll(QPerson.person.age.eq(CERSEI.getAge()), page1.nextPageable());

		assertThat(page2.getTotalElements()).isEqualTo(2L);
		assertThat(page2.getContent()).hasSize(1);
		assertThat(page2.hasNext()).isFalse();
	}

	@Test // DATACMNS-525
	void findAllUsingOrderSpecifierWorksCorrectly() {

		Iterable<Person> result = repository.findAll(QPerson.person.age.eq(CERSEI.getAge()),
				QPerson.person.firstname.desc());

		assertThat(result).containsExactly(JAIME, CERSEI);
	}

	@Test // DATACMNS-525
	void findAllUsingPageableWithSortWorksCorrectly() {

		Iterable<Person> result = repository.findAll(QPerson.person.age.eq(CERSEI.getAge()),
				PageRequest.of(0, 10, Direction.DESC, "firstname"));

		assertThat(result).containsExactly(JAIME, CERSEI);
	}

	@Test // DATACMNS-525
	void findAllUsingPagableWithQSortWorksCorrectly() {

		Iterable<Person> result = repository.findAll(QPerson.person.age.eq(CERSEI.getAge()),
				PageRequest.of(0, 10, new QSort(QPerson.person.firstname.desc())));

		assertThat(result).containsExactly(JAIME, CERSEI);
	}

	@Test // DATAKV-90
	void findAllWithOrderSpecifierWorksCorrectly() {

		Iterable<Person> result = repository.findAll(new QSort(QPerson.person.firstname.desc()));

		assertThat(result).containsExactly(TYRION, JAIME, CERSEI);
	}

	@Test // DATAKV-90, DATAKV-197
	void findAllShouldRequireSort() {
		assertThatIllegalArgumentException().isThrownBy(() -> repository.findAll((QSort) null));
	}

	@Test // DATAKV-90, DATAKV-197
	void findAllShouldAllowUnsortedFindAll() {

		Iterable<Person> result = repository.findAll(Sort.unsorted());

		assertThat(result).contains(TYRION, JAIME, CERSEI);
	}

	@Test // DATAKV-95
	void executesExistsCorrectly() {
		assertThat(repository.exists(QPerson.person.age.eq(CERSEI.getAge()))).isTrue();
	}

	@Test // DATAKV-96
	void shouldSupportFindAllWithPredicateAndSort() {

		List<Person> users = Streamable.of(repository.findAll(person.age.gt(0), Sort.by(Direction.ASC, "firstname")))
				.toList();

		assertThat(users).hasSize(3);
		assertThat(users.get(0).getFirstname()).isEqualTo(CERSEI.getFirstname());
		assertThat(users.get(2).getFirstname()).isEqualTo(TYRION.getFirstname());
		assertThat(users).contains(CERSEI, JAIME, TYRION);
	}

	@Test // DATAKV-179
	void throwsExceptionIfMoreThanOneResultIsFound() {

		assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class) //
				.isThrownBy(() -> repository.findOne(person.firstname.contains("e")));
	}

	@Test // GH-397
	void findByShouldReturnFirst() {

		Person first = repository.findBy(QPerson.person.firstname.eq("tyrion"),
				FluentQuery.FetchableFluentQuery::firstValue);

		assertThat(first).isEqualTo(TYRION);

		first = repository.findBy(QPerson.person.firstname.eq("foo"), Function.identity()).firstValue();

		assertThat(first).isNull();
	}

	@Test // GH-397
	void findByShouldReturnOne() {

		assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class)
				.isThrownBy(() -> repository.findBy(QPerson.person.firstname.ne("foo"), FluentQuery.FetchableFluentQuery::one));

		Person one = repository.findBy(QPerson.person.firstname.eq("tyrion"), FluentQuery.FetchableFluentQuery::oneValue);

		assertThat(one).isEqualTo(TYRION);
	}

	@Test // GH-397
	void findByShouldReturnFirstWithProjection() {

		PersonProjection interfaceProjection = repository.findBy(QPerson.person.firstname.eq("tyrion"),
				it -> it.as(PersonProjection.class).firstValue());
		assertThat(interfaceProjection.getFirstname()).isEqualTo("tyrion");

		PersonDto dto = repository.findBy(QPerson.person.firstname.eq("tyrion"), it -> it.as(PersonDto.class).firstValue());
		assertThat(dto.getFirstname()).isEqualTo("tyrion");
	}

	@Test // GH-397
	void findByShouldReturnOneWithProjection() {

		PersonProjection interfaceProjection = repository.findBy(QPerson.person.firstname.eq("tyrion"),
				it -> it.as(PersonProjection.class).oneValue());
		assertThat(interfaceProjection.getFirstname()).isEqualTo("tyrion");

		PersonDto dto = repository.findBy(QPerson.person.firstname.eq("tyrion"), it -> it.as(PersonDto.class).oneValue());
		assertThat(dto.getFirstname()).isEqualTo("tyrion");
	}

	@Test // GH-397
	void findByShouldReturnAll() {

		List<Person> all = repository.findBy(QPerson.person.firstname.eq("tyrion"), FluentQuery.FetchableFluentQuery::all);

		assertThat(all).contains(TYRION);
	}

	@Test // GH-397
	void findByShouldReturnAllSorted() {

		List<Person> all = repository.findBy(QPerson.person.firstname.ne("foo"),
				q -> q.sortBy(Sort.by(Direction.ASC, "firstname")).all());

		assertThat(all).containsSequence(CERSEI, JAIME, TYRION);

		all = repository.findBy(QPerson.person.firstname.ne("foo"),
				q -> q.sortBy(Sort.by(Direction.DESC, "firstname")).all());

		assertThat(all).containsSequence(TYRION, JAIME, CERSEI);
	}

	@Test // GH-397
	void findByShouldReturnAllWithProjection() {

		Stream<PersonProjection> all = repository.findBy(QPerson.person.firstname.eq("tyrion"),
				q -> q.as(PersonProjection.class).stream());

		assertThat(all).hasOnlyElementsOfType(PersonProjection.class);
	}

	@Test // GH-397
	void findByShouldReturnPage() {

		Page<PersonProjection> page = repository.findBy(QPerson.person.firstname.ne("foo"),
				it -> it.as(PersonProjection.class).page(PageRequest.of(0, 1, Sort.by("firstname"))));

		assertThat(page.getContent().get(0).getFirstname()).isEqualTo("cersei");
		assertThat(page.getTotalPages()).isEqualTo(3);

		Page<PersonProjection> nextPage = repository.findBy(QPerson.person.firstname.ne("foo"),
				it -> it.as(PersonProjection.class).page(page.nextPageable()));

		assertThat(nextPage.getContent().get(0).getFirstname()).isEqualTo("jaime");
		assertThat(nextPage.getTotalPages()).isEqualTo(3);
	}

	@Test // GH-397
	void findByShouldReturnStream() {

		List<Person> all = repository.findBy(QPerson.person.firstname.eq("tyrion"), FluentQuery.FetchableFluentQuery::all);

		assertThat(all).contains(TYRION);
	}

	@Test // GH-397
	void findByShouldReturnStreamWithProjection() {

		Stream<PersonProjection> all = repository.findBy(QPerson.person.firstname.eq("tyrion"),
				q -> q.as(PersonProjection.class).stream());

		assertThat(all).hasOnlyElementsOfType(PersonProjection.class);
	}

	@Test // GH-397
	void findByShouldReturnCount() {

		long count = repository.findBy(QPerson.person.firstname.ne("foo"), FluentQuery.FetchableFluentQuery::count);

		assertThat(count).isEqualTo(3);
	}

	@Test // GH-397
	void findByShouldReturnExists() {

		boolean exists = repository.findBy(QPerson.person.firstname.eq("tyrion"), FluentQuery.FetchableFluentQuery::exists);
		assertThat(exists).isTrue();

		exists = repository.findBy(QPerson.person.firstname.eq("foo"), FluentQuery.FetchableFluentQuery::exists);
		assertThat(exists).isFalse();
	}

	interface PersonProjection {
		String getFirstname();
	}

	static class PersonDto {

		String firstname;

		public String getFirstname() {
			return this.firstname;
		}

		public void setFirstname(String firstname) {
			this.firstname = firstname;
		}
	}

	@Override
	protected QPersonRepository getRepository(KeyValueRepositoryFactory factory) {
		return factory.getRepository(QPersonRepository.class);
	}

	interface QPersonRepository extends org.springframework.data.map.AbstractRepositoryUnitTests.PersonRepository,
			QuerydslPredicateExecutor<Person> {}
}