Skip to content

Commit 58eef8c

Browse files
committed
Refinements in logic
Signed-off-by: Emilien Bevierre <emilien.bevierre@couchbase.com>
1 parent 9a0a73a commit 58eef8c

4 files changed

Lines changed: 58 additions & 7 deletions

File tree

src/main/java/org/springframework/data/couchbase/core/ReactiveFindBySearchOperationSupport.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ static class ReactiveFindBySearchSupport<T> implements ReactiveFindBySearch<T> {
104104

105105
@Override
106106
public TerminatingFindBySearch<T> matching(SearchRequest searchRequest) {
107-
Assert.notNull(searchRequest, "SearchRequest must not be null!");
107+
Assert.notNull(searchRequest, "SearchRequest must not be null");
108108
return new ReactiveFindBySearchSupport<>(template, domainType, returnType, indexName, searchRequest,
109109
scanConsistency, scope, collection, options, sort, highlightStyle, highlightFields, facets,
110110
fields, limitSkip, support);
@@ -120,7 +120,7 @@ public FindBySearchWithProjection<T> withIndex(String indexName) {
120120

121121
@Override
122122
public <R> FindBySearchWithFields<R> as(final Class<R> returnType) {
123-
Assert.notNull(returnType, "returnType must not be null!");
123+
Assert.notNull(returnType, "returnType must not be null");
124124
return new ReactiveFindBySearchSupport<>(template, domainType, returnType, indexName, searchRequest,
125125
scanConsistency, scope, collection, options, sort, highlightStyle, highlightFields, facets,
126126
fields, limitSkip, support);
@@ -316,18 +316,20 @@ private Mono<SearchResult<T>> collectFullResult(ReactiveSearchResult reactiveRes
316316
}
317317

318318
/**
319-
* Hydrates a SearchRow into an entity via KV GET, silently skipping documents that have been
319+
* Hydrates a SearchRow into an entity via KV GET, skipping documents that have been
320320
* deleted between the FTS index update and the KV fetch (stale index entries).
321+
* <p>
322+
* Misses are logged at WARN so operators can detect index staleness; persistent or high-volume
323+
* misses typically indicate an out-of-sync FTS index.
321324
*/
322325
private Mono<T> hydrateRow(SearchRow row) {
323326
return template.findById(returnType)
324327
.inScope(scope)
325328
.inCollection(collection)
326329
.one(row.id())
327330
.onErrorResume(com.couchbase.client.core.error.DocumentNotFoundException.class, ex -> {
328-
if (LOG.isDebugEnabled()) {
329-
LOG.debug("Skipping stale FTS result for document id '{}': document not found in KV", row.id());
330-
}
331+
LOG.warn("Skipping stale FTS result for document id '{}': document not found in KV "
332+
+ "(index '{}' may be out of sync)", row.id(), indexName);
331333
return Mono.empty();
332334
});
333335
}

src/main/java/org/springframework/data/couchbase/repository/Search.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,18 @@
3030
* The FTS index name must be specified via {@link SearchIndex} on the method or entity class.
3131
* <p>
3232
* Supports positional parameter placeholders ({@code ?0}, {@code ?1}, etc.) which are replaced with method argument
33-
* values at execution time. Example:
33+
* values at execution time.
34+
* <p>
35+
* <strong>Parameter binding semantics (injection considerations):</strong> String and {@link Enum} values are always
36+
* emitted as quoted FTS phrases ({@code "..."}), with embedded {@code "} and {@code \} escaped. This prevents a
37+
* parameter value from breaking out of its phrase context and injecting FTS operators ({@code AND}, {@code OR},
38+
* {@code :}, {@code *}, field selectors, etc.). {@link Number} and {@link Boolean} values are emitted verbatim so they
39+
* remain usable with range operators (e.g. {@code rating:>=?0}); their types are validated, so no operator injection
40+
* is possible. Because string parameters are always phrase-quoted, there is no way to pass a <em>raw</em> FTS term or
41+
* operator through a placeholder. Construct a {@link com.couchbase.client.java.search.SearchRequest} and use the
42+
* template API ({@code CouchbaseOperations.findBySearch(...)}) if you need that.
43+
* <p>
44+
* Example:
3445
* <pre>
3546
* &#64;Search("description:pool AND city:\"San Francisco\"")
3647
* &#64;SearchIndex("hotel-search-index")

src/main/java/org/springframework/data/couchbase/repository/query/ReactiveSearchBasedCouchbaseQuery.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
* Supports positional parameter substitution ({@code ?0}, {@code ?1}, etc.) in the query string.
3838
* Supports {@link Pageable} parameters for limit/skip pagination.
3939
* Supports {@link org.springframework.data.couchbase.repository.ScanConsistency} for FTS scan consistency.
40+
* <p>
41+
* {@link org.springframework.data.domain.Page} and {@link org.springframework.data.domain.Slice} return types are
42+
* <em>not</em> supported on reactive search methods. Use the imperative repository or the reactive template API
43+
* ({@link org.springframework.data.couchbase.core.ReactiveFindBySearchOperation.TerminatingFindBySearch#result()})
44+
* if you need total row counts alongside entities.
4045
*
4146
* @author Emilien Bevierre
4247
* @since 6.2
@@ -86,6 +91,13 @@ public ReactiveCouchbaseQueryMethod getQueryMethod() {
8691
}
8792

8893
private Object executeDependingOnType(ParametersParameterAccessor accessor, SearchRequest request) {
94+
if (method.isPageQuery() || method.isSliceQuery()) {
95+
throw new UnsupportedOperationException(
96+
"Page and Slice return types are not supported for reactive @Search repository methods. "
97+
+ "Use the imperative repository for pagination, or the reactive template API "
98+
+ "(ReactiveCouchbaseOperations.findBySearch(...).result()) to access total rows.");
99+
}
100+
89101
ReactiveFindBySearchOperation.FindBySearchWithQuery<?> queryOp = createQueryOperation(accessor, true);
90102

91103
if (method.isCountQuery()) {

src/test/java/org/springframework/data/couchbase/repository/query/ReactiveSearchBasedCouchbaseQueryTests.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
import org.springframework.data.couchbase.domain.User;
3737
import org.springframework.data.couchbase.repository.Search;
3838
import org.springframework.data.couchbase.repository.SearchIndex;
39+
import org.springframework.data.domain.Page;
40+
import org.springframework.data.domain.PageRequest;
41+
import org.springframework.data.domain.Pageable;
42+
import org.springframework.data.domain.Slice;
3943
import org.springframework.data.domain.Sort;
4044
import org.springframework.data.mapping.context.MappingContext;
4145
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
@@ -93,6 +97,20 @@ void executeResolvesReactiveWrapperParameters() {
9397
assertEquals("\"match\"", capturedQuery.get());
9498
}
9599

100+
@Test
101+
void rejectsPageReturnTypeAtConstruction() {
102+
Throwable thrown = org.junit.jupiter.api.Assertions.assertThrows(RuntimeException.class,
103+
() -> createQueryMethod("searchPaged", "match", PageRequest.of(0, 10)));
104+
org.junit.jupiter.api.Assertions.assertInstanceOf(InvalidDataAccessApiUsageException.class, thrown.getCause());
105+
}
106+
107+
@Test
108+
void rejectsSliceReturnTypeAtConstruction() {
109+
Throwable thrown = org.junit.jupiter.api.Assertions.assertThrows(RuntimeException.class,
110+
() -> createQueryMethod("searchSliced", "match", PageRequest.of(0, 10)));
111+
org.junit.jupiter.api.Assertions.assertInstanceOf(InvalidDataAccessApiUsageException.class, thrown.getCause());
112+
}
113+
96114
@Test
97115
void executeRejectsSpringSortParameters() {
98116
ReactiveSearchBasedCouchbaseQuery query = new ReactiveSearchBasedCouchbaseQuery(
@@ -117,6 +135,14 @@ interface TestReactiveSearchRepository extends ReactiveCrudRepository<User, Stri
117135
@Search("?0")
118136
@SearchIndex("test-index")
119137
Flux<User> searchSorted(String query, Sort sort);
138+
139+
@Search("?0")
140+
@SearchIndex("test-index")
141+
Mono<Page<User>> searchPaged(String query, Pageable pageable);
142+
143+
@Search("?0")
144+
@SearchIndex("test-index")
145+
Mono<Slice<User>> searchSliced(String query, Pageable pageable);
120146
}
121147

122148
interface NameOnly {

0 commit comments

Comments
 (0)