From 6c4b327bba4e523d244fa9eac81503fb96e1301f Mon Sep 17 00:00:00 2001 From: Brian Sam-Bodden Date: Sat, 13 Sep 2025 10:05:04 -0700 Subject: [PATCH] feat: add SearchStream return type support for repository methods Add support for repositories to return SearchStream for fluent query operations. This enables combining Spring Data repository methods with Entity Stream capabilities for both JSON documents and Redis Hashes. Changes: - Implement SearchStream detection and handling in RediSearchQuery for JSON documents - Implement SearchStream detection and handling in RedisEnhancedQuery for Redis Hashes - Create EntityStream instances that are configured with query filters - Support lazy execution when terminal operations are called Documentation: - Add Entity Streams Integration section to hash-mappings.adoc - Update comparison table to mention SearchStream return capability This feature allows repository methods to return SearchStream enabling: - Fluent filtering with field predicates - Mapping and transformation operations - Sorting, limiting, and counting - Lazy evaluation for better performance --- .../modules/ROOT/pages/hash-mappings.adoc | 71 +++++- .../repository/query/RediSearchQuery.java | 30 ++- .../repository/query/RedisEnhancedQuery.java | 32 ++- .../hash/model/HashWithSearchStream.java | 50 ++++ .../HashWithSearchStreamRepository.java | 24 ++ .../SearchStreamHashRepositoryTest.java | 206 ++++++++++++++++ .../SearchStreamRepositoryTest.java | 219 ++++++++++++++++++ 7 files changed, 627 insertions(+), 5 deletions(-) create mode 100644 tests/src/test/java/com/redis/om/spring/fixtures/hash/model/HashWithSearchStream.java create mode 100644 tests/src/test/java/com/redis/om/spring/fixtures/hash/repository/HashWithSearchStreamRepository.java create mode 100644 tests/src/test/java/com/redis/om/spring/repository/SearchStreamHashRepositoryTest.java create mode 100644 tests/src/test/java/com/redis/om/spring/repository/SearchStreamRepositoryTest.java diff --git a/docs/content/modules/ROOT/pages/hash-mappings.adoc b/docs/content/modules/ROOT/pages/hash-mappings.adoc index 38f7cd04..39f580a4 100644 --- a/docs/content/modules/ROOT/pages/hash-mappings.adoc +++ b/docs/content/modules/ROOT/pages/hash-mappings.adoc @@ -54,7 +54,7 @@ Redis OM Spring creates full RediSearch indexes using `FT.CREATE` commands, prov |Query Methods |Limited to findBy patterns -|Complex queries, @Query, Entity Streams +|Complex queries, @Query, Entity Streams, SearchStream returns |=== == Basic Usage @@ -425,6 +425,75 @@ List admins = entityStream .collect(Collectors.toList()); ---- +=== Entity Streams Integration with Repositories + +Repositories can return `SearchStream` for fluent query operations: + +[source,java] +---- +import com.redis.om.spring.search.stream.SearchStream; + +public interface PersonRepository extends RedisEnhancedRepository { + // Return SearchStream for advanced operations + SearchStream findByDepartment(String department); + + SearchStream findByAgeGreaterThan(int age); + + SearchStream findByActive(boolean active); + + // Usage example: + // SearchStream stream = repository.findByDepartment("Engineering"); + // List names = stream + // .filter(Person$.ACTIVE.eq(true)) + // .map(Person$.NAME) + // .collect(Collectors.toList()); +} +---- + +This allows you to combine repository query methods with the power of Entity Streams: + +[source,java] +---- +@Service +public class PersonService { + @Autowired + PersonRepository repository; + + public List getActiveEngineerNames() { + return repository.findByDepartment("Engineering") + .filter(Person$.ACTIVE.eq(true)) + .map(Person$.NAME) + .sorted() + .collect(Collectors.toList()); + } + + public long countSeniorEmployees(int minAge) { + return repository.findByAgeGreaterThan(minAge) + .filter(Person$.DEPARTMENT.in("Engineering", "Management")) + .count(); + } + + public List getTopPerformers() { + return repository.findByActive(true) + .filter(Person$.PERFORMANCE_SCORE.gte(90)) + .sorted(Person$.PERFORMANCE_SCORE, SortOrder.DESC) + .limit(10) + .collect(Collectors.toList()); + } +} +---- + +The `SearchStream` returned by repository methods supports all Entity Stream operations: + +* **Filtering**: `filter()` with field predicates +* **Mapping**: `map()` to transform results +* **Sorting**: `sorted()` with field and order +* **Limiting**: `limit()` to restrict results +* **Aggregation**: `count()`, `findFirst()`, `anyMatch()`, `allMatch()` +* **Collection**: `collect()` to lists, sets, or custom collectors + +NOTE: Fields used in SearchStream operations must be properly indexed with `@Indexed`, `@Searchable`, or other indexing annotations. + == Time To Live (TTL) You can set expiration times for entities: diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/RediSearchQuery.java b/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/RediSearchQuery.java index adf9c63d..c330a1d9 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/RediSearchQuery.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/RediSearchQuery.java @@ -47,6 +47,9 @@ import com.redis.om.spring.repository.query.countmin.CountMinQueryExecutor; import com.redis.om.spring.repository.query.cuckoo.CuckooQueryExecutor; import com.redis.om.spring.repository.query.lexicographic.LexicographicQueryExecutor; +import com.redis.om.spring.search.stream.EntityStream; +import com.redis.om.spring.search.stream.EntityStreamImpl; +import com.redis.om.spring.search.stream.SearchStream; import com.redis.om.spring.util.ObjectUtils; import redis.clients.jedis.search.FieldName; @@ -165,6 +168,7 @@ private static FieldType getRedisFieldTypeForMapValue(Class fieldType) { private final LexicographicQueryExecutor lexicographicQueryExecutor; private final GsonBuilder gsonBuilder; private final RediSearchIndexer indexer; + private final EntityStream entityStream; private RediSearchQueryType type; private String value; // query fields @@ -234,6 +238,7 @@ public RediSearchQuery(// this.domainType = this.queryMethod.getEntityInformation().getJavaType(); this.gsonBuilder = gsonBuilder; this.redisOMProperties = redisOMProperties; + this.entityStream = new EntityStreamImpl(modulesOperations, gsonBuilder, indexer); bloomQueryExecutor = new BloomQueryExecutor(this, modulesOperations); cuckooQueryExecutor = new CuckooQueryExecutor(this, modulesOperations); @@ -887,8 +892,29 @@ private Object executeQuery(Object[] parameters) { // what to return Object result = null; - // Check if this is an exists query - if (processor.getReturnedType().getReturnedType() == boolean.class || processor.getReturnedType() + // Check if this is a SearchStream query + if (SearchStream.class.isAssignableFrom(queryMethod.getReturnedObjectType())) { + // For SearchStream, create and configure a stream based on the query + @SuppressWarnings( + "unchecked" + ) SearchStream stream = entityStream.of((Class) domainType); + + // Build the query string using the existing query builder + String queryString = prepareQuery(parameters, true); + + // Apply the filter if it's not a wildcard query + if (!queryString.equals("*") && !queryString.isEmpty()) { + stream = stream.filter(queryString); + } + + // Apply limit if configured + if (limit != null && limit > 0) { + stream = stream.limit(limit); + } + + // Return the configured stream + return stream; + } else if (processor.getReturnedType().getReturnedType() == boolean.class || processor.getReturnedType() .getReturnedType() == Boolean.class) { // For exists queries, return true if we have any results, false otherwise result = searchResult.getTotalResults() > 0; diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/RedisEnhancedQuery.java b/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/RedisEnhancedQuery.java index ee78c400..02889d62 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/RedisEnhancedQuery.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/RedisEnhancedQuery.java @@ -34,6 +34,7 @@ import org.springframework.util.ReflectionUtils; import com.github.f4b6a3.ulid.Ulid; +import com.google.gson.GsonBuilder; import com.redis.om.spring.RedisOMProperties; import com.redis.om.spring.annotations.*; import com.redis.om.spring.convert.MappingRedisOMConverter; @@ -45,6 +46,9 @@ import com.redis.om.spring.repository.query.clause.QueryClause; import com.redis.om.spring.repository.query.countmin.CountMinQueryExecutor; import com.redis.om.spring.repository.query.cuckoo.CuckooQueryExecutor; +import com.redis.om.spring.search.stream.EntityStream; +import com.redis.om.spring.search.stream.EntityStreamImpl; +import com.redis.om.spring.search.stream.SearchStream; import com.redis.om.spring.util.ObjectUtils; import redis.clients.jedis.search.FieldName; @@ -115,6 +119,7 @@ public class RedisEnhancedQuery implements RepositoryQuery { private final RedisModulesOperations modulesOperations; private final MappingRedisOMConverter mappingConverter; private final RediSearchIndexer indexer; + private final EntityStream entityStream; private final BloomQueryExecutor bloomQueryExecutor; private final CuckooQueryExecutor cuckooQueryExecutor; private final CountMinQueryExecutor countMinQueryExecutor; @@ -190,6 +195,8 @@ public RedisEnhancedQuery(QueryMethod queryMethod, // this.redisOMProperties = redisOMProperties; this.redisOperations = redisOperations; this.mappingConverter = new MappingRedisOMConverter(null, new ReferenceResolverImpl(redisOperations)); + // Create EntityStream with a default GsonBuilder since we're dealing with hashes + this.entityStream = new EntityStreamImpl(modulesOperations, new GsonBuilder(), indexer); bloomQueryExecutor = new BloomQueryExecutor(this, modulesOperations); cuckooQueryExecutor = new CuckooQueryExecutor(this, modulesOperations); @@ -577,8 +584,29 @@ private Object executeQuery(Object[] parameters) { // what to return Object result; - // Check if this is an exists query - if (processor.getReturnedType().getReturnedType() == boolean.class || processor.getReturnedType() + // Check if this is a SearchStream query + if (SearchStream.class.isAssignableFrom(queryMethod.getReturnedObjectType())) { + // For SearchStream, create and configure a stream based on the query + @SuppressWarnings( + "unchecked" + ) SearchStream stream = entityStream.of((Class) domainType); + + // Build the query string using the existing query builder + String queryString = prepareQuery(parameters, true); + + // Apply the filter if it's not a wildcard query + if (!queryString.equals("*") && !queryString.isEmpty()) { + stream = stream.filter(queryString); + } + + // Apply limit if configured + if (limit != null && limit > 0) { + stream = stream.limit(limit); + } + + // Return the configured stream + return stream; + } else if (processor.getReturnedType().getReturnedType() == boolean.class || processor.getReturnedType() .getReturnedType() == Boolean.class) { // For exists queries, return true if we have any results, false otherwise result = searchResult.getTotalResults() > 0; diff --git a/tests/src/test/java/com/redis/om/spring/fixtures/hash/model/HashWithSearchStream.java b/tests/src/test/java/com/redis/om/spring/fixtures/hash/model/HashWithSearchStream.java new file mode 100644 index 00000000..14b3adc3 --- /dev/null +++ b/tests/src/test/java/com/redis/om/spring/fixtures/hash/model/HashWithSearchStream.java @@ -0,0 +1,50 @@ +package com.redis.om.spring.fixtures.hash.model; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +import com.redis.om.spring.annotations.Indexed; +import com.redis.om.spring.annotations.Searchable; + +import lombok.*; + +/** + * Test entity for SearchStream with properly indexed fields + */ +@Data +@NoArgsConstructor(force = true) +@RequiredArgsConstructor(staticName = "of") +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@RedisHash("hash_with_search_stream") +public class HashWithSearchStream { + + @Id + String id; + + @NonNull + @Searchable + String name; + + @NonNull + @Indexed + String email; + + @NonNull + @Indexed + String department; + + @NonNull + @Indexed + Integer age; + + @NonNull + @Indexed + Boolean active; + + @NonNull + @Indexed + Set skills = new HashSet<>(); +} \ No newline at end of file diff --git a/tests/src/test/java/com/redis/om/spring/fixtures/hash/repository/HashWithSearchStreamRepository.java b/tests/src/test/java/com/redis/om/spring/fixtures/hash/repository/HashWithSearchStreamRepository.java new file mode 100644 index 00000000..f0447eaa --- /dev/null +++ b/tests/src/test/java/com/redis/om/spring/fixtures/hash/repository/HashWithSearchStreamRepository.java @@ -0,0 +1,24 @@ +package com.redis.om.spring.fixtures.hash.repository; + +import java.util.Set; + +import org.springframework.stereotype.Repository; + +import com.redis.om.spring.fixtures.hash.model.HashWithSearchStream; +import com.redis.om.spring.repository.RedisEnhancedRepository; +import com.redis.om.spring.search.stream.SearchStream; + +@Repository +public interface HashWithSearchStreamRepository extends RedisEnhancedRepository { + + // Methods that return SearchStream for testing + SearchStream findByEmail(String email); + + SearchStream findByDepartment(String department); + + SearchStream findByAgeGreaterThan(Integer age); + + SearchStream findByActive(Boolean active); + + SearchStream findBySkills(Set skills); +} \ No newline at end of file diff --git a/tests/src/test/java/com/redis/om/spring/repository/SearchStreamHashRepositoryTest.java b/tests/src/test/java/com/redis/om/spring/repository/SearchStreamHashRepositoryTest.java new file mode 100644 index 00000000..6a9ad154 --- /dev/null +++ b/tests/src/test/java/com/redis/om/spring/repository/SearchStreamHashRepositoryTest.java @@ -0,0 +1,206 @@ +package com.redis.om.spring.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import com.redis.om.spring.AbstractBaseEnhancedRedisTest; +import com.redis.om.spring.fixtures.hash.model.HashWithSearchStream; +import com.redis.om.spring.fixtures.hash.model.HashWithSearchStream$; +import com.redis.om.spring.fixtures.hash.repository.HashWithSearchStreamRepository; +import com.redis.om.spring.search.stream.SearchStream; + +/** + * Test to verify that hash repositories can return SearchStream for fluent query operations. + * This validates that SearchStream works for both JSON documents and Redis Hash entities. + */ +class SearchStreamHashRepositoryTest extends AbstractBaseEnhancedRedisTest { + + @Autowired + HashWithSearchStreamRepository repository; + + private HashWithSearchStream john; + private HashWithSearchStream jane; + private HashWithSearchStream bob; + private HashWithSearchStream alice; + private HashWithSearchStream charlie; + + @BeforeEach + void setUp() { + // Create test people with properly indexed fields + john = HashWithSearchStream.of("John Doe", "john@example.com", "Engineering", 35, true); + john.setSkills(Set.of("Java", "Spring", "Redis")); + + jane = HashWithSearchStream.of("Jane Smith", "jane@example.com", "Marketing", 28, true); + jane.setSkills(Set.of("SEO", "Content", "Analytics")); + + bob = HashWithSearchStream.of("Bob Johnson", "bob@example.com", "Engineering", 42, false); + bob.setSkills(Set.of("Python", "Docker", "Kubernetes")); + + alice = HashWithSearchStream.of("Alice Williams", "alice@example.com", "HR", 31, true); + alice.setSkills(Set.of("Recruiting", "Training", "Compliance")); + + charlie = HashWithSearchStream.of("Charlie Brown", "charlie@example.com", "Engineering", 55, false); + charlie.setSkills(Set.of("Java", "Architecture", "Microservices")); + + repository.saveAll(List.of(john, jane, bob, alice, charlie)); + } + + @AfterEach + void tearDown() { + repository.deleteAll(); + } + + @Test + void testHashRepositoryReturnsSearchStream() { + // Test that repository method returns SearchStream for hash entities + SearchStream stream = repository.findByEmail("john@example.com"); + + assertNotNull(stream, "Repository should return a SearchStream"); + assertThat(stream).isInstanceOf(SearchStream.class); + + // Verify the stream contains the expected person + List people = stream.collect(Collectors.toList()); + assertEquals(1, people.size(), "Should find 1 person with john@example.com email"); + assertEquals("John Doe", people.get(0).getName()); + } + + @Test + void testHashSearchStreamFluentOperations() { + // Test fluent operations on SearchStream returned from repository + SearchStream stream = repository.findByDepartment("Engineering"); + + // Further filter by active status + List activePeople = stream + .filter(HashWithSearchStream$.ACTIVE.eq(true)) + .collect(Collectors.toList()); + + assertEquals(1, activePeople.size(), "Should find 1 active person in Engineering"); + assertEquals("John Doe", activePeople.get(0).getName()); + } + + @Test + void testHashSearchStreamMapOperation() { + // Test map operation on SearchStream to extract emails + SearchStream stream = repository.findByDepartment("Engineering"); + + // Map to names + List names = stream + .map(HashWithSearchStream$.NAME) + .collect(Collectors.toList()); + + assertEquals(3, names.size(), "Should find 3 people in Engineering"); + assertThat(names).containsExactlyInAnyOrder("John Doe", "Bob Johnson", "Charlie Brown"); + } + + @Test + void testHashSearchStreamChainedFilters() { + // Test multiple chained filter operations + SearchStream stream = repository.findByAgeGreaterThan(30); + + // Chain multiple filters - age > 30 and active + List filteredPeople = stream + .filter(HashWithSearchStream$.ACTIVE.eq(true)) + .collect(Collectors.toList()); + + assertEquals(2, filteredPeople.size(), "Should find 2 active people over 30"); + + List names = filteredPeople.stream() + .map(HashWithSearchStream::getName) + .sorted() + .collect(Collectors.toList()); + + assertThat(names).containsExactly("Alice Williams", "John Doe"); + } + + @Test + void testHashSearchStreamWithSort() { + // Test sorting capabilities of SearchStream + SearchStream stream = repository.findByDepartment("Engineering"); + + // Sort by age ascending + List sortedPeople = stream + .sorted(HashWithSearchStream$.AGE, redis.clients.jedis.search.aggr.SortedField.SortOrder.ASC) + .collect(Collectors.toList()); + + assertEquals(3, sortedPeople.size()); + + // Verify sorting order by age + assertEquals("John Doe", sortedPeople.get(0).getName()); // 35 + assertEquals("Bob Johnson", sortedPeople.get(1).getName()); // 42 + assertEquals("Charlie Brown", sortedPeople.get(2).getName()); // 55 + } + + @Test + void testHashSearchStreamLimit() { + // Test limit operation on SearchStream + SearchStream stream = repository.findByDepartment("Engineering"); + + // Limit to first 2 results + List limitedPeople = stream + .limit(2) + .collect(Collectors.toList()); + + assertEquals(2, limitedPeople.size(), "Should return only 2 people due to limit"); + } + + @Test + void testHashSearchStreamCount() { + // Test count operation on SearchStream + SearchStream stream = repository.findByDepartment("Engineering"); + + long count = stream.count(); + + assertEquals(3, count, "Should count 3 people in Engineering department"); + } + + @Test + void testHashSearchStreamEmptyResult() { + // Test SearchStream with no matching results + SearchStream stream = repository.findByEmail("nonexistent@example.com"); + + List people = stream.collect(Collectors.toList()); + + assertTrue(people.isEmpty(), "Should return empty list for nonexistent email"); + } + + @Test + void testHashSearchStreamComplexQuery() { + // Test a complex query combining multiple operations + SearchStream stream = repository.findByActive(true); + + // Complex query: active people, filter by department, map to emails + List emails = stream + .filter(HashWithSearchStream$.DEPARTMENT.in("Engineering", "HR")) + .map(HashWithSearchStream$.EMAIL) + .collect(Collectors.toList()); + + assertEquals(2, emails.size(), "Should find 2 active people in Engineering or HR"); + assertThat(emails).containsExactlyInAnyOrder("john@example.com", "alice@example.com"); + } + + @Test + void testHashSearchStreamWithSkills() { + // Test SearchStream with Set field (skills) + SearchStream stream = repository.findBySkills(Set.of("Java")); + + List javaDevs = stream.collect(Collectors.toList()); + + assertEquals(2, javaDevs.size(), "Should find 2 people with Java skill"); + + List names = javaDevs.stream() + .map(HashWithSearchStream::getName) + .sorted() + .collect(Collectors.toList()); + + assertThat(names).containsExactly("Charlie Brown", "John Doe"); + } +} \ No newline at end of file diff --git a/tests/src/test/java/com/redis/om/spring/repository/SearchStreamRepositoryTest.java b/tests/src/test/java/com/redis/om/spring/repository/SearchStreamRepositoryTest.java new file mode 100644 index 00000000..ec35b346 --- /dev/null +++ b/tests/src/test/java/com/redis/om/spring/repository/SearchStreamRepositoryTest.java @@ -0,0 +1,219 @@ +package com.redis.om.spring.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDate; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.geo.Point; + +import com.redis.om.spring.AbstractBaseDocumentTest; +import com.redis.om.spring.fixtures.document.model.Company; +import com.redis.om.spring.fixtures.document.model.Company$; +import com.redis.om.spring.fixtures.document.model.CompanyMeta; +import com.redis.om.spring.fixtures.document.model.Employee; +import com.redis.om.spring.fixtures.document.repository.CompanyRepository; +import com.redis.om.spring.search.stream.SearchStream; +import redis.clients.jedis.search.aggr.SortedField.SortOrder; + +/** + * Test to verify that repositories can return SearchStream for fluent query operations. + * This validates the documentation claim that "Repositories can return SearchStream for fluent query operations" + */ +class SearchStreamRepositoryTest extends AbstractBaseDocumentTest { + + @Autowired + CompanyRepository repository; + + private Company redis; + private Company microsoft; + private Company apple; + private Company ibm; + private Company oracle; + + @BeforeEach + void setUp() { + // Create test companies with various founding years + redis = Company.of("RedisInc", 2011, LocalDate.of(2021, 5, 1), + new Point(-122.066540, 37.377690), "stack@redis.com"); + redis.setTags(Set.of("database", "nosql", "redis")); + redis.setMetaList(Set.of(CompanyMeta.of("Redis", 100, Set.of("RedisTag")))); + redis.setPubliclyListed(false); + redis.setEmployees(Set.of(Employee.of("John Doe"), Employee.of("Jane Smith"))); + + microsoft = Company.of("Microsoft", 1975, LocalDate.of(2022, 8, 15), + new Point(-122.124500, 47.640160), "research@microsoft.com"); + microsoft.setTags(Set.of("software", "cloud", "windows")); + microsoft.setMetaList(Set.of(CompanyMeta.of("MS", 50, Set.of("MsTag")))); + microsoft.setPubliclyListed(true); + + apple = Company.of("Apple", 1976, LocalDate.of(2022, 9, 10), + new Point(-122.0322, 37.3220), "info@apple.com"); + apple.setTags(Set.of("hardware", "software", "mobile")); + apple.setMetaList(Set.of(CompanyMeta.of("AAPL", 75, Set.of("AppleTag")))); + apple.setPubliclyListed(true); + + ibm = Company.of("IBM", 1911, LocalDate.of(2022, 6, 1), + new Point(-73.8007, 41.0504), "contact@ibm.com"); + ibm.setTags(Set.of("enterprise", "cloud", "ai")); + ibm.setPubliclyListed(true); + + oracle = Company.of("Oracle", 1977, LocalDate.of(2022, 7, 1), + new Point(-122.2659, 37.5314), "info@oracle.com"); + oracle.setTags(Set.of("database", "enterprise", "cloud")); + oracle.setPubliclyListed(true); + + repository.saveAll(List.of(redis, microsoft, apple, ibm, oracle)); + } + + @AfterEach + void tearDown() { + repository.deleteAll(); + } + + @Test + void testRepositoryReturnsSearchStream() { + // Test that repository method returns SearchStream + SearchStream stream = repository.findByYearFoundedGreaterThan(1970); + + assertNotNull(stream, "Repository should return a SearchStream"); + assertThat(stream).isInstanceOf(SearchStream.class); + + // Verify the stream contains the expected companies + List companies = stream.collect(Collectors.toList()); + assertEquals(4, companies.size(), "Should find 4 companies founded after 1970"); + + // Verify companies are: Microsoft (1975), Apple (1976), Oracle (1977), RedisInc (2011) + List companyNames = companies.stream() + .map(Company::getName) + .sorted() + .collect(Collectors.toList()); + + assertThat(companyNames).containsExactly("Apple", "Microsoft", "Oracle", "RedisInc"); + } + + @Test + void testSearchStreamFluentOperations() { + // Test fluent operations on SearchStream returned from repository + SearchStream stream = repository.findByYearFoundedGreaterThan(1970); + + // Filter for publicly listed companies only + List publicCompanies = stream + .filter(Company$.PUBLICLY_LISTED.eq(true)) + .collect(Collectors.toList()); + + assertEquals(3, publicCompanies.size(), "Should find 3 publicly listed companies"); + + // Verify the publicly listed companies + List publicCompanyNames = publicCompanies.stream() + .map(Company::getName) + .sorted() + .collect(Collectors.toList()); + + assertThat(publicCompanyNames).containsExactly("Apple", "Microsoft", "Oracle"); + } + + @Test + void testSearchStreamMapOperation() { + // Test map operation on SearchStream to extract company names + SearchStream stream = repository.findByYearFoundedGreaterThan(2000); + + // Map to company names + List companyNames = stream + .map(Company$.NAME) + .collect(Collectors.toList()); + + assertEquals(1, companyNames.size(), "Should find 1 company founded after 2000"); + assertEquals("RedisInc", companyNames.get(0)); + } + + @Test + void testSearchStreamChainedFilters() { + // Test multiple chained filter operations + SearchStream stream = repository.findByYearFoundedGreaterThan(1900); + + // Filter for publicly listed companies with "database" tag + List databaseCompanies = stream + .filter(Company$.PUBLICLY_LISTED.eq(true)) + .filter(Company$.TAGS.in("database")) + .collect(Collectors.toList()); + + assertEquals(1, databaseCompanies.size(), "Should find 1 publicly listed database company"); + assertEquals("Oracle", databaseCompanies.get(0).getName()); + } + + @Test + void testSearchStreamWithSort() { + // Test sorting capabilities of SearchStream + SearchStream stream = repository.findByYearFoundedGreaterThan(1970); + + // Sort by year founded ascending + List sortedCompanies = stream + .sorted(Company$.YEAR_FOUNDED, SortOrder.ASC) + .collect(Collectors.toList()); + + assertEquals(4, sortedCompanies.size()); + + // Verify sorting order + assertEquals("Microsoft", sortedCompanies.get(0).getName()); // 1975 + assertEquals("Apple", sortedCompanies.get(1).getName()); // 1976 + assertEquals("Oracle", sortedCompanies.get(2).getName()); // 1977 + assertEquals("RedisInc", sortedCompanies.get(3).getName()); // 2011 + } + + @Test + void testSearchStreamLimit() { + // Test limit operation on SearchStream + SearchStream stream = repository.findByYearFoundedGreaterThan(1900); + + // Limit to first 2 results + List limitedCompanies = stream + .limit(2) + .collect(Collectors.toList()); + + assertEquals(2, limitedCompanies.size(), "Should return only 2 companies due to limit"); + } + + @Test + void testSearchStreamCount() { + // Test count operation on SearchStream + SearchStream stream = repository.findByYearFoundedGreaterThan(1970); + + long count = stream.count(); + + assertEquals(4, count, "Should count 4 companies founded after 1970"); + } + + @Test + void testSearchStreamEmptyResult() { + // Test SearchStream with no matching results + SearchStream stream = repository.findByYearFoundedGreaterThan(2020); + + List companies = stream.collect(Collectors.toList()); + + assertTrue(companies.isEmpty(), "Should return empty list for companies founded after 2020"); + } + + @Test + void testSearchStreamComplexQuery() { + // Test a complex query combining multiple operations as shown in documentation + SearchStream stream = repository.findByYearFoundedGreaterThan(1970); + + // Complex query: publicly listed companies, map to names, filter names starting with 'A' + List names = stream + .filter(Company$.PUBLICLY_LISTED.eq(true)) + .map(Company$.NAME) + .filter(name -> ((String) name).startsWith("A")) + .collect(Collectors.toList()); + + assertEquals(1, names.size(), "Should find 1 publicly listed company starting with 'A'"); + assertEquals("Apple", names.get(0)); + } +} \ No newline at end of file