Skip to content

Commit 0127d0c

Browse files
authored
NCO-61: Update async analytics API to current spec (#56)
* Refactor async query types for spec compliance: * Replace QueryStatus and QueryHandleResults with QueryResultHandle * Remove handle serialization logic * Adopt fluent builder options (`FetchResultHandleOptions`, etc.) * Drop `RequestTimeout`, `ResultTTL`, and `Deserializer` from start options * Add `Scope.StartQueryAsync()` to support context-mapped async queries * Map 404s to `QueryNotFoundException` on fetches, and treat as success for cancel and discard operations * Update unit and func tests for options, 404s, and end-to-end flows
1 parent 5be0c1f commit 0127d0c

21 files changed

Lines changed: 1014 additions & 630 deletions

src/Couchbase.Analytics/Async/QueryHandle.cs

Lines changed: 25 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,21 @@
2121

2222
using System.Text.Json;
2323
using Couchbase.AnalyticsClient.Internal;
24+
using Couchbase.AnalyticsClient.Options;
25+
using Couchbase.AnalyticsClient.Results;
2426

2527
namespace Couchbase.AnalyticsClient.Async;
2628

2729
/// <summary>
2830
/// Represents a handle to a server-side asynchronous query.
29-
/// Obtained from <see cref="Cluster.StartQueryAsync"/> or <see cref="Cluster.QueryHandleFromSerialized"/>.
31+
/// Obtained from <see cref="Cluster.StartQueryAsync"/>.
3032
/// </summary>
3133
public class QueryHandle
3234
{
3335
private readonly IAnalyticsService _analyticsService;
34-
private readonly TimeSpan? _requestTimeout;
3536

3637
/// <summary>
37-
/// The query handle string used to poll status and fetch results.
38-
/// This is the path segment after <c>/api/v1/request/status/</c>.
38+
/// The query handle string used to poll for the result handle.
3939
/// </summary>
4040
public string Handle { get; }
4141

@@ -44,75 +44,53 @@ public class QueryHandle
4444
/// </summary>
4545
public string RequestId { get; }
4646

47-
internal QueryHandle(string handle, string requestId, IAnalyticsService analyticsService, TimeSpan? requestTimeout = null)
47+
internal QueryHandle(string handle, string requestId, IAnalyticsService analyticsService)
4848
{
4949
Handle = handle ?? throw new ArgumentNullException(nameof(handle));
5050
RequestId = requestId ?? throw new ArgumentNullException(nameof(requestId));
5151
_analyticsService = analyticsService ?? throw new ArgumentNullException(nameof(analyticsService));
52-
_requestTimeout = requestTimeout;
5352
}
5453

5554
/// <summary>
56-
/// Fetches the current status of the asynchronous query from the server.
55+
/// Fetches the result handle of the asynchronous query from the server.
5756
/// </summary>
57+
/// <param name="options">Options for fetching the result handle.</param>
5858
/// <param name="cancellationToken">A cancellation token.</param>
59-
/// <returns>A <see cref="QueryStatus"/> representing the current state of the query.</returns>
60-
public async Task<QueryStatus> FetchStatusAsync(CancellationToken cancellationToken = default)
59+
/// <returns>A <see cref="QueryResultHandle"/> if results are ready, otherwise null.</returns>
60+
public Task<QueryResultHandle?> FetchResultHandleAsync(FetchResultHandleOptions? options = null, CancellationToken cancellationToken = default)
6161
{
62-
return await _analyticsService.FetchStatusAsync(Handle, _requestTimeout, cancellationToken)
63-
.ConfigureAwait(false);
62+
options ??= new FetchResultHandleOptions();
63+
return _analyticsService.FetchResultHandleAsync(this, options, cancellationToken);
6464
}
6565

6666
/// <summary>
67-
/// Discards the query results on the server. After this call, the results can no longer be fetched.
67+
/// Fetches the result handle of the asynchronous query from the server.
6868
/// </summary>
69-
/// <param name="cancellationToken">A cancellation token.</param>
70-
public async Task DiscardResultsAsync(CancellationToken cancellationToken = default)
69+
public Task<QueryResultHandle?> FetchResultHandleAsync(Func<FetchResultHandleOptions, FetchResultHandleOptions> options, CancellationToken cancellationToken = default)
7170
{
72-
await _analyticsService.DiscardResultsAsync(Handle, _requestTimeout, cancellationToken)
73-
.ConfigureAwait(false);
71+
var fetchOptions = new FetchResultHandleOptions();
72+
fetchOptions = options.Invoke(fetchOptions);
73+
return FetchResultHandleAsync(fetchOptions, cancellationToken);
7474
}
7575

7676
/// <summary>
7777
/// Cancels the query on the server. If the query has already completed, this is a no-op.
7878
/// </summary>
79+
/// <param name="options">Options for cancellation.</param>
7980
/// <param name="cancellationToken">A cancellation token.</param>
80-
public async Task CancelAsync(CancellationToken cancellationToken = default)
81-
{
82-
await _analyticsService.CancelQueryAsync(RequestId, _requestTimeout, cancellationToken)
83-
.ConfigureAwait(false);
84-
}
85-
86-
/// <summary>
87-
/// Serializes this <see cref="QueryHandle"/> to a JSON string so it can be persisted and
88-
/// later reconstructed via <see cref="Cluster.QueryHandleFromSerialized"/>.
89-
/// This method does not perform any network operations.
90-
/// </summary>
91-
/// <returns>A JSON string containing the handle and request ID.</returns>
92-
public string Serialize()
81+
public Task CancelAsync(CancelOptions? options = null, CancellationToken cancellationToken = default)
9382
{
94-
var data = new SerializedQueryHandle(Handle, RequestId);
95-
return JsonSerializer.Serialize(data);
83+
options ??= new CancelOptions();
84+
return _analyticsService.CancelQueryAsync(RequestId, options, cancellationToken);
9685
}
9786

9887
/// <summary>
99-
/// Deserializes a <see cref="QueryHandle"/> from a JSON string previously produced by <see cref="Serialize"/>.
100-
/// This method does not perform any network operations.
88+
/// Cancels the query on the server. If the query has already completed, this is a no-op.
10189
/// </summary>
102-
internal static QueryHandle Deserialize(string serializedHandle, IAnalyticsService analyticsService, TimeSpan? requestTimeout = null)
90+
public Task CancelAsync(Func<CancelOptions, CancelOptions> options, CancellationToken cancellationToken = default)
10391
{
104-
ArgumentNullException.ThrowIfNull(serializedHandle);
105-
106-
var data = JsonSerializer.Deserialize<SerializedQueryHandle>(serializedHandle)
107-
?? throw new ArgumentException("Invalid serialized handle format.", nameof(serializedHandle));
108-
109-
if (string.IsNullOrWhiteSpace(data.Handle) || string.IsNullOrWhiteSpace(data.RequestId))
110-
{
111-
throw new ArgumentException("Serialized handle is missing required fields.", nameof(serializedHandle));
112-
}
113-
114-
return new QueryHandle(data.Handle, data.RequestId, analyticsService, requestTimeout);
92+
var cancelOptions = new CancelOptions();
93+
cancelOptions = options.Invoke(cancelOptions);
94+
return CancelAsync(cancelOptions, cancellationToken);
11595
}
116-
117-
private record SerializedQueryHandle(string Handle, string RequestId);
11896
}

src/Couchbase.Analytics/Async/QueryHandleResults.cs

Lines changed: 0 additions & 58 deletions
This file was deleted.

src/Couchbase.Analytics/Async/QueryPartition.cs

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#region License
2+
/* ************************************************************
3+
*
4+
* @author Couchbase <[email protected]>
5+
* @copyright 2025 Couchbase, Inc.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*
19+
* ************************************************************/
20+
#endregion
21+
22+
using Couchbase.AnalyticsClient.Internal;
23+
using Couchbase.AnalyticsClient.Options;
24+
using Couchbase.AnalyticsClient.Results;
25+
26+
namespace Couchbase.AnalyticsClient.Async;
27+
28+
/// <summary>
29+
/// Provides access to the results of a completed asynchronous query.
30+
/// </summary>
31+
public class QueryResultHandle
32+
{
33+
private readonly string _handlePath;
34+
private readonly IAnalyticsService _analyticsService;
35+
36+
/// <summary>
37+
/// The request ID assigned by the server when the query was submitted.
38+
/// </summary>
39+
public string RequestId { get; }
40+
41+
internal QueryResultHandle(string handlePath, string requestId, IAnalyticsService analyticsService)
42+
{
43+
_handlePath = handlePath ?? throw new ArgumentNullException(nameof(handlePath));
44+
RequestId = requestId ?? throw new ArgumentNullException(nameof(requestId));
45+
_analyticsService = analyticsService ?? throw new ArgumentNullException(nameof(analyticsService));
46+
}
47+
48+
/// <summary>
49+
/// Fetches the results of the query from the server.
50+
/// </summary>
51+
/// <param name="options">Options for fetching the results.</param>
52+
/// <param name="cancellationToken">A cancellation token.</param>
53+
/// <returns>An <see cref="IQueryResult"/> that can be used to enumerate the result rows.</returns>
54+
public Task<IQueryResult> FetchResultsAsync(FetchResultsOptions? options = null, CancellationToken cancellationToken = default)
55+
{
56+
options ??= new FetchResultsOptions();
57+
return _analyticsService.FetchResultsAsync(RequestId, _handlePath, options, cancellationToken);
58+
}
59+
60+
/// <summary>
61+
/// Fetches the results of the query from the server.
62+
/// </summary>
63+
public Task<IQueryResult> FetchResultsAsync(Func<FetchResultsOptions, FetchResultsOptions> options, CancellationToken cancellationToken = default)
64+
{
65+
var fetchOptions = new FetchResultsOptions();
66+
fetchOptions = options.Invoke(fetchOptions);
67+
return FetchResultsAsync(fetchOptions, cancellationToken);
68+
}
69+
70+
/// <summary>
71+
/// Discards the query results on the server. After this call, the results can no longer be fetched.
72+
/// </summary>
73+
/// <param name="options">Options for discarding results.</param>
74+
/// <param name="cancellationToken">A cancellation token.</param>
75+
public Task DiscardResultsAsync(DiscardResultsOptions? options = null, CancellationToken cancellationToken = default)
76+
{
77+
options ??= new DiscardResultsOptions();
78+
return _analyticsService.DiscardResultsAsync(RequestId, _handlePath, options, cancellationToken);
79+
}
80+
81+
/// <summary>
82+
/// Discards the query results on the server. After this call, the results can no longer be fetched.
83+
/// </summary>
84+
public Task DiscardResultsAsync(Func<DiscardResultsOptions, DiscardResultsOptions> options, CancellationToken cancellationToken = default)
85+
{
86+
var discardOptions = new DiscardResultsOptions();
87+
discardOptions = options.Invoke(discardOptions);
88+
return DiscardResultsAsync(discardOptions, cancellationToken);
89+
}
90+
}

0 commit comments

Comments
 (0)