You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Refactor how we build pagination response to support collections in SQL (#3521)
## Why make this change?
Closes#3462
## What is this change?
Refactors how SQL Find responses signal pagination so it no longer
relies on the JSON shape of the result list.
Previously, `SqlResponseHelpers.FormatFindResult` appended pagination
metadata to the row collection as an **in-band sentinel** a
single-element JSON array wrapping `{ "nextLink": ... }` (REST) or `{
"after": ... }` (MCP) and `OkResponse` recovered it by checking whether
the last element of the result list was a `JsonValueKind.Array`. This
worked only because SQL backends never produced array-shaped row values.
That assumption breaks with SQL Server's `JSON` type, `JSON_ARRAY()`,
`vector`, and other collection-shaped column values a real data row
could carry an array-shaped tail value and be misclassified as a
pagination envelope.
This change carries pagination metadata **out-of-band**:
- `FormatFindResult` builds the response envelope directly as a typed
projection (`{ value, nextLink }` for REST, `{ value, after }` for MCP)
using a `bool hasNext` local instead of encoding the decision into the
row collection.
- `OkResponse(JsonElement, bool?)` is reduced to a single-responsibility
envelope wrapper. **The signature is preserved** (`isMcpRequest` is now
optional and accepted-but-unused) so existing callers of
`Microsoft.DataApiBuilder.Core` continue to compile and link unchanged.
The shape-detection branch is removed from the body.
- `GetConsolidatedNextLinkForPagination` is preserved with its original
signature and `JsonElement` return type for source compatibility.
Internally, `FormatFindResult` now constructs the next-page URL inline
(it needs the URL as a `string` to put into the typed response
envelope), avoiding a serialize-then-parse round-trip just to satisfy
the old wrapper shape.
- `DetermineExtraFieldsInResponse` gains a `JsonValueKind.Object` guard
so future array/scalar/collection-typed row shapes return an empty
extra-fields set rather than throwing on `EnumerateObject()`.
- Drive-by cleanup: removed a dead `!` null-forgiving operator,
standardized `JsonValueKind` checks on `is`, refreshed stale comments
and XML docs, removed dead commented-out code.
**Wire format is unchanged** for both REST and MCP. **Public API surface
is byte-identical to `main`** no signatures added, removed, or renamed;
no `[Obsolete]` warnings emitted to downstream consumers. The new gate:
```csharp
bool hasNext = isCollection && SqlPaginationUtil.HasNext(...)
```
is logically equivalent to the original gate.
## How was this tested?
Added seven focused unit tests in `SqlResponseHelpersUnitTests.cs`
pinning the envelope shape across every branch:
1. Empty result
2. Single object
3. Collection without next page
4. Collection with next page (REST → `nextLink`)
5. Collection with next page (MCP → `after`)
6. Round-trip of a row containing an array-valued column
(forward-looking coverage for SQL Server JSON/vector column shapes)
7. **Regression guard** for the actual pre-refactor failure mode: a
result whose last top-level element is itself a JSON array the exact
trigger the old in-band sentinel detection used is now correctly
preserved as ordinary data instead of being unpacked as a pagination
envelope.
All seven pass. `dotnet format --verify-no-changes` is clean across the
touched files.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
/// <param name="httpContext">HTTP context associated with the API request</param>
34
-
/// <param name="isMcpRequest">True if request is done through MCP endpoint</param>
34
+
/// <param name="isMcpRequest"><c>true</c> if invoked from the MCP endpoint (emit <c>after</c>); <c>false</c> or <c>null</c> for REST (emit <c>nextLink</c>).</param>
35
35
/// <returns>An OkObjectResult from a Find operation that has been correctly formatted.</returns>
36
36
publicstaticOkObjectResultFormatFindResult(
37
37
JsonElementfindOperationResponse,
@@ -41,49 +41,48 @@ public static OkObjectResult FormatFindResult(
41
41
HttpContexthttpContext,
42
42
bool?isMcpRequest=null)
43
43
{
44
-
45
-
// When there are no rows returned from the database, the jsonElement will be an empty array.
46
-
// In that case, the response is returned as is.
44
+
// Empty result set: return the standard envelope { "value": [] } and skip extra-field/cursor work.
// If the results are not a collection or if the query does not have a next page
60
-
// no nextLink/after is needed. So, the response is returned after removing the extra fields.
61
-
if(findOperationResponse.ValueKindis not JsonValueKind.Array||!SqlPaginationUtil.HasNext(findOperationResponse,context.First,defaultPageSize,maxPageSize))
61
+
// No-pagination path: single object, or a collection without a next page.
62
+
if(!hasNext)
62
63
{
63
-
// If there are no additional fields present, the response is returned directly. When there
64
-
// are extra fields, they are removed before returning the response.
@@ -139,9 +136,17 @@ public static OkObjectResult FormatFindResult(
139
136
/// </summary>
140
137
/// <param name="response">Response json retrieved from the database</param>
141
138
/// <param name="fieldsToBeReturned">List of fields to be returned in the response.</param>
142
-
/// <returns>Additional fields that are present in the response</returns>
139
+
/// <returns>Additional fields that are present in the response. Returns an empty set when <paramref name="response"/> is not a JSON object (e.g. a scalar or array-typed row value), since there are no named properties to filter.</returns>
0 commit comments