Include next_url in default table JSON keys

Table JSON responses previously only included the next pagination token
by default - the ready-to-follow next_url required ?_extra=next_url.
Both keys are now always present (null on the final page), which the
pagination documentation already claimed. The next_url extra remains
valid for backwards compatibility.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GrHZSypDfMnym1tM5XJAFZ
This commit is contained in:
Claude 2026-07-04 16:19:03 +00:00
commit 0bf3a54716
No known key found for this signature in database
5 changed files with 33 additions and 2 deletions

View file

@ -2318,6 +2318,7 @@ async def table_view_data(
data = {
"ok": True,
"next": next_value and str(next_value) or None,
"next_url": next_url,
}
data.update(
await resolve_table_extras(

View file

@ -48,7 +48,7 @@ The ``"rows"`` key is a list of objects, each one representing a row.
The ``"truncated"`` key lets you know if the query was truncated. This can happen if a SQL query returns more than 1,000 results (or the :ref:`setting_max_returned_rows` setting).
For table pages, an additional key ``"next"`` may be present. This indicates that the next page in the pagination set can be retrieved using ``?_next=VALUE``.
For table pages, two additional keys are present: ``"next"``, an opaque token that can be used to retrieve the next page using ``?_next=TOKEN``, and ``"next_url"``, the full URL of that next page. Both are ``null`` on the final page. See :ref:`json_api_pagination`.
.. _json_api_errors:
@ -128,6 +128,7 @@ options:
{
"ok": true,
"next": null,
"next_url": null,
"rows": [
[3, "Detroit"],
[2, "Los Angeles"],

View file

@ -647,6 +647,7 @@ dispatched to `QueryView` (views/table.py:1703-1712).
|---|---|
| `ok` | `true` when data was retrieved without error |
| `next` | pagination token string, or `null` on the last page |
| `next_url` | absolute URL of the next page, or `null` on the last page |
| `rows` | list of row objects `{column: value}` (default `_shape=objects`) |
| `truncated` | always present; `false` for table pages |

View file

@ -196,7 +196,14 @@ number.
---
## 3. Pagination: three mechanisms, two contracts (P2)
## 3. Pagination: three mechanisms, two contracts (P2) — partially implemented
> **Status:** `next_url` now accompanies `next` in the default table JSON
> keys (previously it required `?_extra=next_url`), so every response with
> a `next` token also carries the ready-to-follow URL. Pagination tokens
> are deliberately left undocumented as to their internal structure. The
> `_size`/`page_size` naming and `has_more`/`total` differences remain
> open.
| Endpoint | Mechanism | Token | Extras |
|---|---|---|---|

View file

@ -1491,6 +1491,7 @@ async def test_col_nocol_errors(ds_client, path, expected_error):
{
"ok": True,
"next": None,
"next_url": None,
"columns": ["id", "content", "content2"],
"rows": [{"id": "1", "content": "hey", "content2": "world"}],
"truncated": False,
@ -1501,6 +1502,7 @@ async def test_col_nocol_errors(ds_client, path, expected_error):
{
"ok": True,
"next": None,
"next_url": None,
"rows": [{"id": "1", "content": "hey", "content2": "world"}],
"truncated": False,
"count": 1,
@ -1627,3 +1629,22 @@ async def test_count_truncated_included_with_count_extra(tmp_path_factory):
assert response.json()["count_truncated"] is True
finally:
ds.close()
@pytest.mark.asyncio
async def test_next_url_included_by_default(ds_client):
response = await ds_client.get("/fixtures/compound_three_primary_keys.json")
data = response.json()
assert data["next"] is not None
assert data["next_url"].endswith(
"/fixtures/compound_three_primary_keys.json?_next="
+ urllib.parse.quote(data["next"], safe="")
)
# Follow to the last page - next and next_url are both null there
while data["next"]:
response = await ds_client.get(
"/fixtures/compound_three_primary_keys.json?_next=" + data["next"]
)
data = response.json()
assert data["next"] is None
assert data["next_url"] is None