Skip to content

Commit 2be1827

Browse files
ebyhrkevinjqliu
andauthored
REST: Add pagination support for list_tables (#3348)
<!-- Thanks for opening a pull request! --> <!-- In the case this PR will resolve an issue, please replace ${GITHUB_ISSUE_ID} below with the actual Github issue id. --> <!-- Closes #${GITHUB_ISSUE_ID} --> # Rationale for this change Follows REST catalog spec: - /v1/{prefix}/tables: https://github.com/apache/iceberg/blob/e7a5a87f26f9de5b200254155aa037368b13a29c/open-api/rest-catalog-open-api.yaml#L525-L562 - ListTablesResponse: https://github.com/apache/iceberg/blob/e7a5a87f26f9de5b200254155aa037368b13a29c/open-api/rest-catalog-open-api.yaml#L4210-L4219 - PageToken: https://github.com/apache/iceberg/blob/e7a5a87f26f9de5b200254155aa037368b13a29c/open-api/rest-catalog-open-api.yaml#L2233-L2256 ## Are these changes tested? Yes, includes unit tests for paginated cases. ## Are there any user-facing changes? - Before: returned incomplete table list when server paginated (only first page) - After: returns complete table list (fetches all pages) <!-- In the case of user-facing changes, please add the changelog label. --> --------- Co-authored-by: Kevin Liu <kevinjqliu@users.noreply.github.com>
1 parent 62e393f commit 2be1827

2 files changed

Lines changed: 108 additions & 6 deletions

File tree

pyiceberg/catalog/rest/__init__.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ class ListViewResponseEntry(IcebergBaseModel):
383383

384384
class ListTablesResponse(IcebergBaseModel):
385385
identifiers: list[ListTableResponseEntry] = Field()
386+
next_page_token: str | None = Field(default=None, alias="next-page-token")
386387

387388

388389
class ListViewsResponse(IcebergBaseModel):
@@ -1039,12 +1040,29 @@ def list_tables(self, namespace: str | Identifier) -> list[Identifier]:
10391040
self._check_endpoint(Capability.V1_LIST_TABLES)
10401041
namespace_tuple = self._check_valid_namespace_identifier(namespace)
10411042
namespace_concat = self._encode_namespace_path(namespace_tuple)
1042-
response = self._session.get(self.url(Endpoints.list_tables, namespace=namespace_concat))
1043-
try:
1044-
response.raise_for_status()
1045-
except HTTPError as exc:
1046-
_handle_non_200_response(exc, {404: NoSuchNamespaceError})
1047-
return [(*table.namespace, table.name) for table in ListTablesResponse.model_validate_json(response.text).identifiers]
1043+
url = self.url(Endpoints.list_tables, namespace=namespace_concat)
1044+
1045+
tables: list[Identifier] = []
1046+
page_token: str | None = None
1047+
1048+
while True:
1049+
params: dict[str, str] = {}
1050+
if page_token:
1051+
params["pageToken"] = page_token
1052+
response = self._session.get(url, params=params)
1053+
try:
1054+
response.raise_for_status()
1055+
except HTTPError as exc:
1056+
_handle_non_200_response(exc, {404: NoSuchNamespaceError})
1057+
1058+
parsed = ListTablesResponse.model_validate_json(response.text)
1059+
tables.extend([(*table.namespace, table.name) for table in parsed.identifiers])
1060+
1061+
if not parsed.next_page_token:
1062+
break
1063+
page_token = parsed.next_page_token
1064+
1065+
return tables
10481066

10491067
@retry(**_RETRY_ARGS)
10501068
@override

tests/catalog/test_rest.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,90 @@ def test_list_tables_200(rest_mock: Mocker) -> None:
480480
assert RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).list_tables(namespace) == [("examples", "fooshare")]
481481

482482

483+
def test_list_tables_paginated_200(rest_mock: Mocker) -> None:
484+
namespace = "examples"
485+
# First page with next-page-token
486+
rest_mock.get(
487+
f"{TEST_URI}v1/namespaces/{namespace}/tables",
488+
json={
489+
"identifiers": [
490+
{"namespace": ["examples"], "name": "table1"},
491+
{"namespace": ["examples"], "name": "table2"},
492+
],
493+
"next-page-token": "page2token",
494+
},
495+
status_code=200,
496+
request_headers=TEST_HEADERS,
497+
)
498+
# Second page with next-page-token
499+
rest_mock.get(
500+
f"{TEST_URI}v1/namespaces/{namespace}/tables?pageToken=page2token",
501+
json={
502+
"identifiers": [
503+
{"namespace": ["examples"], "name": "table3"},
504+
],
505+
"next-page-token": "page3token",
506+
},
507+
status_code=200,
508+
request_headers=TEST_HEADERS,
509+
)
510+
# Third page without next-page-token (last page)
511+
rest_mock.get(
512+
f"{TEST_URI}v1/namespaces/{namespace}/tables?pageToken=page3token",
513+
json={
514+
"identifiers": [
515+
{"namespace": ["examples"], "name": "table4"},
516+
],
517+
},
518+
status_code=200,
519+
request_headers=TEST_HEADERS,
520+
)
521+
522+
result = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).list_tables(namespace)
523+
assert result == [
524+
("examples", "table1"),
525+
("examples", "table2"),
526+
("examples", "table3"),
527+
("examples", "table4"),
528+
]
529+
530+
531+
def test_list_tables_paginated_200_none_next_page_token(rest_mock: Mocker) -> None:
532+
namespace = "examples"
533+
# First page with next-page-token
534+
rest_mock.get(
535+
f"{TEST_URI}v1/namespaces/{namespace}/tables",
536+
json={
537+
"identifiers": [
538+
{"namespace": ["examples"], "name": "table1"},
539+
{"namespace": ["examples"], "name": "table2"},
540+
],
541+
"next-page-token": "page2token",
542+
},
543+
status_code=200,
544+
request_headers=TEST_HEADERS,
545+
)
546+
# The last page with NONE next-page-token
547+
rest_mock.get(
548+
f"{TEST_URI}v1/namespaces/{namespace}/tables?pageToken=page2token",
549+
json={
550+
"identifiers": [
551+
{"namespace": ["examples"], "name": "table3"},
552+
],
553+
"next-page-token": None,
554+
},
555+
status_code=200,
556+
request_headers=TEST_HEADERS,
557+
)
558+
559+
result = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).list_tables(namespace)
560+
assert result == [
561+
("examples", "table1"),
562+
("examples", "table2"),
563+
("examples", "table3"),
564+
]
565+
566+
483567
def test_list_tables_200_sigv4(rest_mock: Mocker) -> None:
484568
namespace = "examples"
485569
rest_mock.get(

0 commit comments

Comments
 (0)