Remove the next_url extra - the key is always present

next_url became a default table JSON key alongside next, making the
extra a no-op. Requesting ?_extra=next_url now returns the standard
unknown-extra 400 error.

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-06 23:59:44 +00:00
commit 4874c29286
No known key found for this signature in database
7 changed files with 23 additions and 43 deletions

View file

@ -107,7 +107,6 @@ class TableContext(Context):
human_description_en: str = from_extra()
is_view: bool = from_extra()
metadata: dict = from_extra()
next_url: str = from_extra()
primary_keys: list = from_extra()
private: bool = from_extra()
query: dict = from_extra()
@ -124,6 +123,11 @@ class TableContext(Context):
metadata={"help": "True if the data for this page was retrieved without errors"}
)
next: str = field(metadata={"help": "Pagination token for the next page, or None"})
next_url: str = field(
metadata={
"help": "Full URL for the next page of results, or None if there are no more pages. See :ref:`json_api_pagination`."
}
)
count_truncated: bool = field(
metadata={
"help": "True if ``count`` is a capped lower bound rather than an exact total, because Datasette stopped counting after its configured row-count limit."

View file

@ -321,21 +321,6 @@ class HumanDescriptionEnExtra(Extra):
return human_description_en
class NextUrlExtra(Extra):
description = "Full URL for the next page of results"
example = ExtraExample(
"/fixtures/facetable.json?_size=1&_extra=next_url",
note=(
"``null`` if there are no more pages of results. "
"See :ref:`json_api_pagination`."
),
)
scopes = {ExtraScope.TABLE}
async def resolve(self, context):
return context.next_url
class ColumnsExtra(Extra):
description = "List of column names returned by this table, row or query."
example = ExtraExample("/fixtures/facetable.json?_extra=columns")
@ -1250,7 +1235,6 @@ TABLE_EXTRA_BUNDLES = {
"count",
"count_sql",
"human_description_en",
"next_url",
"metadata",
"query",
"columns",
@ -1286,7 +1270,6 @@ TABLE_EXTRA_CLASSES = [
SuggestedFacetsExtra,
FacetInstancesProvider,
HumanDescriptionEnExtra,
NextUrlExtra,
ColumnsExtra,
AllColumnsExtra,
PrimaryKeysExtra,

View file

@ -333,7 +333,7 @@ These can be repeated or comma-separated:
::
?_extra=columns&_extra=count,next_url
?_extra=columns&_extra=count,count_sql
Requesting an ``_extra`` name that does not exist returns a ``400`` error in the :ref:`standard error format <json_api_errors>`, for example ``{"ok": false, "error": "Unknown _extra: nope", ...}``.
@ -437,17 +437,6 @@ The available table extras are listed below.
"where state = \"CA\" sorted by pk"
``next_url``
Full URL for the next page of results
``GET /fixtures/facetable.json?_size=1&_extra=next_url``
``null`` if there are no more pages of results. See :ref:`json_api_pagination`.
.. code-block:: json
"http://localhost/fixtures/facetable.json?_size=1&_extra=next_url&_next=1"
``columns``
List of column names returned by this table, row or query.

View file

@ -329,7 +329,7 @@ Many of these keys are shared with the :ref:`JSON API <json_api>` for this page.
Pagination token for the next page, or None
``next_url`` - ``str``
Full URL for the next page of results
Full URL for the next page of results, or None if there are no more pages. See :ref:`json_api_pagination`.
``ok`` - ``bool``
True if the data for this page was retrieved without errors

View file

@ -722,7 +722,7 @@ def test_config_cache_size(app_client_larger_cache_size):
def test_config_force_https_urls():
with make_app_client(settings={"force_https_urls": True}) as client:
response = client.get(
"/fixtures/facetable.json?_size=3&_facet=state&_extra=next_url,suggested_facets"
"/fixtures/facetable.json?_size=3&_facet=state&_extra=suggested_facets"
)
assert response.json["next_url"].startswith("https://")
assert response.json["facet_results"]["results"]["state"]["results"][0][

View file

@ -739,3 +739,11 @@ async def test_sql_interrupted_html_page_keeps_rich_error(ds_client):
)
assert response.status_code == 400
assert "<textarea" in response.text
@pytest.mark.asyncio
async def test_next_url_extra_no_longer_exists(ds_client):
# next_url is always present in table JSON; the extra was removed
response = await ds_client.get("/fixtures/facetable.json?_extra=next_url")
data = assert_canonical_error(response, 400)
assert data["errors"] == ["Unknown _extra: next_url"]

View file

@ -323,10 +323,6 @@ async def test_paginate_tables_and_views(
fetched = []
count = 0
while path:
if "?" in path:
path += "&_extra=next_url"
else:
path += "?_extra=next_url"
response = await ds_client.get(path)
assert response.status_code == 200
count += 1
@ -359,9 +355,7 @@ async def test_validate_page_size(ds_client, path, expected_error):
@pytest.mark.asyncio
async def test_page_size_zero(ds_client):
"""For _size=0 we return the counts, empty rows and no continuation token"""
response = await ds_client.get(
"/fixtures/no_primary_key.json?_size=0&_extra=count,next_url"
)
response = await ds_client.get("/fixtures/no_primary_key.json?_size=0&_extra=count")
assert response.status_code == 200
assert [] == response.json()["rows"]
assert 202 == response.json()["count"]
@ -372,7 +366,7 @@ async def test_page_size_zero(ds_client):
@pytest.mark.asyncio
async def test_paginate_compound_keys(ds_client):
fetched = []
path = "/fixtures/compound_three_primary_keys.json?_shape=objects&_extra=next_url"
path = "/fixtures/compound_three_primary_keys.json?_shape=objects"
page = 0
while path:
page += 1
@ -393,7 +387,9 @@ async def test_paginate_compound_keys(ds_client):
@pytest.mark.asyncio
async def test_paginate_compound_keys_with_extra_filters(ds_client):
fetched = []
path = "/fixtures/compound_three_primary_keys.json?content__contains=d&_shape=objects&_extra=next_url"
path = (
"/fixtures/compound_three_primary_keys.json?content__contains=d&_shape=objects"
)
page = 0
while path:
page += 1
@ -446,7 +442,7 @@ async def test_paginate_compound_keys_with_extra_filters(ds_client):
],
)
async def test_sortable(ds_client, query_string, sort_key, human_description_en):
path = f"/fixtures/sortable.json?_shape=objects&_extra=human_description_en,next_url&{query_string}"
path = f"/fixtures/sortable.json?_shape=objects&_extra=human_description_en&{query_string}"
fetched = []
page = 0
while path:
@ -850,7 +846,7 @@ def test_page_size_matching_max_returned_rows(
app_client_returned_rows_matches_page_size,
):
fetched = []
path = "/fixtures/no_primary_key.json?_extra=next_url"
path = "/fixtures/no_primary_key.json"
while path:
response = app_client_returned_rows_matches_page_size.get(path)
fetched.extend(response.json["rows"])