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