This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new 18acb9ba14 [python] Preserve table UUID in REST rollback requests
(#10528)
18acb9ba14 is described below
commit 18acb9ba14774c07682cf9b6fdeffd0fadec5e0c
Author: QuakeWang <[email protected]>
AuthorDate: Wed Oct 14 09:57:53 2026 +0800
[python] Preserve table UUID in REST rollback requests (#10528)
Signed-off-by: QuakeWang <[email protected]>
---
paimon-python/pypaimon/api/api_request.py | 4 +-
paimon-python/pypaimon/api/rest_api.py | 6 +-
paimon-python/pypaimon/catalog/catalog.py | 6 +-
.../pypaimon/catalog/catalog_environment.py | 4 +-
.../pypaimon/catalog/rest/rest_catalog.py | 7 +-
paimon-python/pypaimon/catalog/table_rollback.py | 9 +-
.../pypaimon/tests/catalog_environment_test.py | 68 +++++
.../pypaimon/tests/rest/rollback_protocol_test.py | 276 +++++++++++++++++++++
8 files changed, 370 insertions(+), 10 deletions(-)
diff --git a/paimon-python/pypaimon/api/api_request.py
b/paimon-python/pypaimon/api/api_request.py
index 04482e9247..7f06c789a3 100644
--- a/paimon-python/pypaimon/api/api_request.py
+++ b/paimon-python/pypaimon/api/api_request.py
@@ -20,7 +20,7 @@ from dataclasses import dataclass
from typing import Dict, List, Optional
from pypaimon.common.identifier import Identifier
-from pypaimon.common.json_util import json_field
+from pypaimon.common.json_util import json_field, optional_json_field
from pypaimon.function.function_change import FunctionChange
from pypaimon.function.function_definition import FunctionDefinition
from pypaimon.management.column_mask import ColumnMask
@@ -106,9 +106,11 @@ class AlterTableRequest(RESTRequest):
class RollbackTableRequest(RESTRequest):
FIELD_INSTANT = "instant"
FIELD_FROM_SNAPSHOT = "fromSnapshot"
+ FIELD_TABLE_ID = "tableId"
instant: Instant = json_field(FIELD_INSTANT)
from_snapshot: Optional[int] = json_field(FIELD_FROM_SNAPSHOT)
+ table_id: Optional[str] = optional_json_field(FIELD_TABLE_ID, "non_null")
@dataclass
diff --git a/paimon-python/pypaimon/api/rest_api.py
b/paimon-python/pypaimon/api/rest_api.py
index d69f49d0dc..d441ea5cbd 100755
--- a/paimon-python/pypaimon/api/rest_api.py
+++ b/paimon-python/pypaimon/api/rest_api.py
@@ -405,7 +405,7 @@ class RESTApi:
)
return response.is_success()
- def rollback_to(self, identifier, instant, from_snapshot=None):
+ def rollback_to(self, identifier, instant, from_snapshot=None, *,
table_uuid=None):
"""Rollback table to the given instant.
Args:
@@ -413,13 +413,15 @@ class RESTApi:
instant: The Instant (SnapshotInstant or TagInstant) to rollback
to.
from_snapshot: Optional snapshot ID. Success only occurs when the
latest snapshot is this snapshot.
+ table_uuid: Expected table or branch UUID from the loaded table
view.
Raises:
NoSuchResourceException: If the table, snapshot or tag does not
exist.
ForbiddenException: If no permission to access this table.
"""
database_name, table_name = self.__validate_identifier(identifier)
- request = RollbackTableRequest(instant=instant,
from_snapshot=from_snapshot)
+ request = RollbackTableRequest(
+ instant=instant, from_snapshot=from_snapshot, table_id=table_uuid)
self.client.post(
self.resource_paths.rollback_table(database_name, table_name),
request,
diff --git a/paimon-python/pypaimon/catalog/catalog.py
b/paimon-python/pypaimon/catalog/catalog.py
index 300d9c7227..1e161abfe8 100644
--- a/paimon-python/pypaimon/catalog/catalog.py
+++ b/paimon-python/pypaimon/catalog/catalog.py
@@ -186,7 +186,8 @@ class Catalog(ABC):
"""
def rollback_to(self, identifier: Union[str, Identifier], instant: Instant,
- from_snapshot: Optional[int] = None) -> None:
+ from_snapshot: Optional[int] = None, *,
+ table_uuid: Optional[str] = None) -> None:
"""Rollback table by the given identifier and instant.
Args:
@@ -194,6 +195,9 @@ class Catalog(ABC):
instant: The Instant (SnapshotInstant or TagInstant) to rollback
to.
from_snapshot: Optional snapshot ID. Success only occurs when the
latest snapshot is this snapshot.
+ table_uuid: Expected table or branch UUID from the loaded table
view.
+ Omit only when its UUID is unavailable. Implementations must
not
+ discard a supplied UUID or reload it by name before rollback.
Raises:
TableNotExistException: If the table does not exist.
diff --git a/paimon-python/pypaimon/catalog/catalog_environment.py
b/paimon-python/pypaimon/catalog/catalog_environment.py
index 0f15594cad..039cc41e98 100644
--- a/paimon-python/pypaimon/catalog/catalog_environment.py
+++ b/paimon-python/pypaimon/catalog/catalog_environment.py
@@ -82,7 +82,7 @@ class CatalogEnvironment:
if self.catalog_loader is not None and
self.supports_version_management:
from pypaimon.catalog.table_rollback import TableRollback
catalog = self.catalog_loader.load()
- return TableRollback(catalog, self.identifier)
+ return TableRollback(catalog, self.identifier, self.uuid)
return None
def snapshot_loader(self) -> Optional[SnapshotLoader]:
@@ -140,7 +140,7 @@ class CatalogEnvironment:
"""
return CatalogEnvironment(
identifier=identifier,
- uuid=self.uuid,
+ uuid=self.uuid if identifier == self.identifier else None,
catalog_loader=self.catalog_loader,
supports_version_management=self.supports_version_management,
rest_table_response=getattr(self, 'rest_table_response', None)
diff --git a/paimon-python/pypaimon/catalog/rest/rest_catalog.py
b/paimon-python/pypaimon/catalog/rest/rest_catalog.py
index 3425de48b0..2e0d5a046e 100644
--- a/paimon-python/pypaimon/catalog/rest/rest_catalog.py
+++ b/paimon-python/pypaimon/catalog/rest/rest_catalog.py
@@ -412,7 +412,8 @@ class RESTCatalog(Catalog):
raise TableNoPermissionException(identifier) from e
def rollback_to(self, identifier: Union[str, Identifier], instant: Instant,
- from_snapshot: Optional[int] = None) -> None:
+ from_snapshot: Optional[int] = None, *,
+ table_uuid: Optional[str] = None) -> None:
"""Rollback table by the given identifier and instant.
Args:
@@ -420,6 +421,7 @@ class RESTCatalog(Catalog):
instant: The Instant (SnapshotInstant or TagInstant) to rollback
to.
from_snapshot: Optional snapshot ID. Success only occurs when the
latest snapshot is this snapshot.
+ table_uuid: Expected table or branch UUID from the loaded table
view.
Raises:
TableNotExistException: If the table does not exist.
@@ -428,7 +430,8 @@ class RESTCatalog(Catalog):
if not isinstance(identifier, Identifier):
identifier = Identifier.from_string(identifier)
try:
- self.rest_api.rollback_to(identifier, instant, from_snapshot)
+ self.rest_api.rollback_to(
+ identifier, instant, from_snapshot, table_uuid=table_uuid)
except NoSuchResourceException as e:
if e.resource_type == ErrorResponse.RESOURCE_TYPE_SNAPSHOT:
raise ValueError(
diff --git a/paimon-python/pypaimon/catalog/table_rollback.py
b/paimon-python/pypaimon/catalog/table_rollback.py
index b609f717b1..50870ab6d8 100644
--- a/paimon-python/pypaimon/catalog/table_rollback.py
+++ b/paimon-python/pypaimon/catalog/table_rollback.py
@@ -18,9 +18,10 @@
class TableRollback:
"""Rollback table to instant by delegating to catalog.rollback_to."""
- def __init__(self, catalog, identifier):
+ def __init__(self, catalog, identifier, table_uuid=None):
self._catalog = catalog
self._identifier = identifier
+ self._table_uuid = table_uuid
def rollback_to(self, instant, from_snapshot=None):
"""Rollback table to the given instant via catalog.
@@ -34,7 +35,11 @@ class TableRollback:
RuntimeError: If the rollback fails.
"""
try:
- self._catalog.rollback_to(self._identifier, instant, from_snapshot)
+ if self._table_uuid is None:
+ self._catalog.rollback_to(self._identifier, instant,
from_snapshot)
+ else:
+ self._catalog.rollback_to(
+ self._identifier, instant, from_snapshot,
table_uuid=self._table_uuid)
except Exception as e:
raise RuntimeError(
"Failed to rollback table {}: {}".format(
diff --git a/paimon-python/pypaimon/tests/catalog_environment_test.py
b/paimon-python/pypaimon/tests/catalog_environment_test.py
index 9ac2e56bf9..4cfa1c56fb 100644
--- a/paimon-python/pypaimon/tests/catalog_environment_test.py
+++ b/paimon-python/pypaimon/tests/catalog_environment_test.py
@@ -36,6 +36,74 @@ class CatalogEnvironmentTest(unittest.TestCase):
_READ_VIA_OPTION = RESTApi.HEADER_PREFIX + RESTApi.READ_VIA_HEADER
+ def test_copy_preserves_uuid_only_for_same_identifier(self):
+ main = Identifier.create("db", "table")
+ explicit_main = Identifier.create("db", "table", branch="main")
+ dev = Identifier.create("db", "table", branch="dev")
+ cases = [
+ (main, main, "source-uuid"),
+ (main, explicit_main, "source-uuid"),
+ (explicit_main, main, "source-uuid"),
+ (dev, dev, "source-uuid"),
+ (main, dev, None),
+ (dev, main, None),
+ (dev, Identifier.create("db", "table", branch="release"), None),
+ (main, Identifier.create("db", "other"), None),
+ (main, Identifier.create("other_db", "table"), None),
+ ]
+ loader = mock.Mock()
+ for source, target, expected_uuid in cases:
+ with self.subTest(source=source, target=target):
+ environment = CatalogEnvironment(
+ identifier=source, uuid="source-uuid",
catalog_loader=loader,
+ supports_version_management=True)
+
+ copied = environment.copy(target)
+
+ self.assertIsNot(copied, environment)
+ self.assertEqual(copied.identifier, target)
+ self.assertEqual(copied.uuid, expected_uuid)
+ self.assertIs(copied.catalog_loader, loader)
+ self.assertTrue(copied.supports_version_management)
+ self.assertEqual(environment.identifier, source)
+ self.assertEqual(environment.uuid, "source-uuid")
+
+ def test_copy_does_not_restore_unknown_uuid(self):
+ main = Identifier.create("db", "table")
+ dev = Identifier.create("db", "table", branch="dev")
+ environment = CatalogEnvironment(identifier=main, uuid="source-uuid")
+
+ self.assertIsNone(environment.copy(dev).copy(main).uuid)
+ unknown = CatalogEnvironment(identifier=main)
+ self.assertIsNone(unknown.copy(main).uuid)
+ self.assertIsNone(unknown.copy(dev).uuid)
+
+ def test_catalog_table_rollback_captures_view_uuid(self):
+ catalog = mock.Mock()
+ identifier = Identifier.create("db", "table", branch="dev")
+ environment = CatalogEnvironment(
+ identifier=identifier, uuid="loaded-uuid",
+ catalog_loader=mock.Mock(load=mock.Mock(return_value=catalog)),
+ supports_version_management=True)
+ rollback = environment.catalog_table_rollback()
+ environment.uuid = "replacement-uuid"
+ instant = mock.sentinel.instant
+
+ rollback.rollback_to(instant, 2)
+
+ catalog.rollback_to.assert_called_once_with(
+ identifier, instant, 2, table_uuid="loaded-uuid")
+
+ def test_snapshot_commit_uses_copied_identity(self):
+ main = Identifier.create("db", "table")
+ environment = CatalogEnvironment(
+ identifier=main, uuid="loaded-uuid", catalog_loader=mock.Mock(),
+ supports_version_management=True)
+ dev = Identifier.create("db", "table", branch="dev")
+
+ self.assertEqual(environment.copy(main).snapshot_commit(None).uuid,
"loaded-uuid")
+ self.assertIsNone(environment.copy(dev).snapshot_commit(None).uuid)
+
def test_dependency_read_context_for_rest_catalog(self):
root = Identifier.create("db", "root", branch="dev")
options = Options({"other-option": "value"})
diff --git a/paimon-python/pypaimon/tests/rest/rollback_protocol_test.py
b/paimon-python/pypaimon/tests/rest/rollback_protocol_test.py
new file mode 100644
index 0000000000..085d251515
--- /dev/null
+++ b/paimon-python/pypaimon/tests/rest/rollback_protocol_test.py
@@ -0,0 +1,276 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import json
+from types import SimpleNamespace
+from urllib.parse import unquote_plus, urlsplit
+
+import pyarrow as pa
+import pytest
+import requests
+
+from pypaimon import Schema
+from pypaimon.api.api_request import RollbackTableRequest
+from pypaimon.api.api_response import GetTableResponse
+from pypaimon.catalog.catalog_context import CatalogContext
+from pypaimon.catalog.catalog_exception import TableNotExistException,
TableNoPermissionException
+from pypaimon.catalog.rest.rest_catalog import RESTCatalog
+from pypaimon.catalog.table_rollback import TableRollback
+from pypaimon.common.identifier import Identifier
+from pypaimon.common.json_util import JSON
+from pypaimon.common.options import Options
+from pypaimon.read.native_plan import _resolved_rest_table_response
+from pypaimon.table.instant import Instant
+from pypaimon.write.commit.commit_rollback import CommitRollback
+from pypaimon.write.native_commit import _rest_catalog_supported
+
+
[email protected]
+def transport(monkeypatch, tmp_path):
+ state = SimpleNamespace(requests=[], uuid='loaded-table-uuid', status=200)
+ schema = Schema.from_pyarrow_schema(pa.schema([('id', pa.int64())]))
+
+ def send(session, request, **kwargs):
+ state.requests.append(request)
+ response = requests.Response()
+ response.request = request
+ response.url = request.url
+ if request.method == 'GET':
+ metadata = GetTableResponse(
+ id=state.uuid,
+ name=unquote_plus(urlsplit(request.url).path.rsplit('/',
1)[-1]),
+ path=tmp_path.as_uri(), is_external=False, schema_id=0,
schema=schema)
+ response.status_code = 200
+ response._content = JSON.to_json(metadata).encode('utf-8')
+ else:
+ response.status_code = state.status
+ response._content = (b'' if state.status == 200 else json.dumps({
+ 'code': state.status, 'message': 'table UUID mismatch',
+ }).encode('utf-8'))
+ return response
+
+ monkeypatch.setattr(requests.Session, 'send', send)
+ return state
+
+
[email protected]
+def catalog(transport):
+ return RESTCatalog(CatalogContext.create_from_options(Options({
+ 'metastore': 'rest', 'uri': 'http://127.0.0.1:1', 'warehouse':
'warehouse',
+ 'prefix': 'rollback-test', 'token.provider': 'bear', 'token':
'test-token',
+ 'data-token.enabled': 'false',
+ })), config_required=False)
+
+
[email protected]
+def table(catalog, transport):
+ loaded = catalog.get_table(Identifier('db', 'table'))
+ transport.requests.clear()
+ return loaded
+
+
+def request_body(request):
+ return json.loads(request.body)
+
+
[email protected]('table_uuid', [None, 'loaded-table-uuid'])
+def test_request_serialization(table_uuid):
+ request = RollbackTableRequest(Instant.snapshot(1), 2, table_id=table_uuid)
+ expected = {'instant': {'type': 'snapshot', 'snapshotId': 1},
'fromSnapshot': 2}
+ if table_uuid is not None:
+ expected['tableId'] = table_uuid
+
+ assert json.loads(JSON.to_json(request)) == expected
+
+
+def test_legacy_request_constructor():
+ assert json.loads(JSON.to_json(RollbackTableRequest(Instant.snapshot(1),
None))) == {
+ 'instant': {'type': 'snapshot', 'snapshotId': 1}, 'fromSnapshot': None,
+ }
+
+
[email protected]('target,instant', [
+ (1, {'type': 'snapshot', 'snapshotId': 1}),
+ ('v1', {'type': 'tag', 'tagName': 'v1'}),
+])
+def test_table_rollback_transmits_loaded_uuid(table, transport, target,
instant):
+ table.rollback_to(target)
+
+ assert len(transport.requests) == 1
+ assert transport.requests[0].method == 'POST'
+ assert request_body(transport.requests[0]) == {
+ 'instant': instant, 'fromSnapshot': None, 'tableId':
'loaded-table-uuid',
+ }
+
+
+def test_compact_rollback_transmits_uuid_and_expected_head(table, transport):
+ rollback =
CommitRollback(table.catalog_environment.catalog_table_rollback())
+
+ assert rollback.try_to_rollback(SimpleNamespace(commit_kind='COMPACT',
id=3))
+
+ assert len(transport.requests) == 1
+ assert request_body(transport.requests[0]) == {
+ 'instant': {'type': 'snapshot', 'snapshotId': 2},
+ 'fromSnapshot': 3, 'tableId': 'loaded-table-uuid',
+ }
+
+
[email protected]('entrypoint', ['catalog', 'api'])
+def test_direct_rollback_accepts_optional_uuid(catalog, transport, entrypoint):
+ delegate = catalog if entrypoint == 'catalog' else catalog.rest_api
+
+ delegate.rollback_to(Identifier('db', 'table'), Instant.snapshot(1), 2,
table_uuid='expected-uuid')
+
+ assert len(transport.requests) == 1
+ assert request_body(transport.requests[0]) == {
+ 'instant': {'type': 'snapshot', 'snapshotId': 1},
+ 'fromSnapshot': 2, 'tableId': 'expected-uuid',
+ }
+
+
+def test_existing_calls_and_encoded_branch_url(catalog, transport):
+ identifier = Identifier('db space', 'table name', branch='dev / blue')
+
+ catalog.rollback_to(identifier, Instant.snapshot(1))
+ catalog.rollback_to(identifier, Instant.snapshot(1), 2)
+ catalog.rollback_to('db.table', Instant.tag('v1'))
+
+ assert [request_body(request)['fromSnapshot'] for request in
transport.requests] == [None, 2, None]
+ assert all(set(request_body(request)) == {'instant', 'fromSnapshot'} for
request in transport.requests)
+ path = urlsplit(transport.requests[0].url).path.split('/')
+ assert len(path) == 8
+ assert unquote_plus(path[4]) == identifier.get_database_name()
+ assert unquote_plus(path[6]) == identifier.get_object_name()
+ assert path[7] == 'rollback'
+
+
+def test_table_rollback_without_uuid(catalog, transport):
+ transport.uuid = None
+ table = catalog.get_table(Identifier('db', 'table'))
+ transport.requests.clear()
+
+ table.rollback_to(1)
+
+ assert len(transport.requests) == 1
+ assert request_body(transport.requests[0]) == {
+ 'instant': {'type': 'snapshot', 'snapshotId': 1}, 'fromSnapshot': None,
+ }
+
+
[email protected]('status,exception', [(403,
TableNoPermissionException), (404, TableNotExistException)])
+def test_existing_error_mapping(catalog, transport, status, exception):
+ transport.status = status
+
+ with pytest.raises(exception):
+ catalog.rollback_to(Identifier('db', 'table'), Instant.snapshot(1),
table_uuid='expected-uuid')
+
+ assert len(transport.requests) == 1
+ assert request_body(transport.requests[0])['tableId'] == 'expected-uuid'
+
+
[email protected]('target', [1, 'v1'])
+def test_replaced_table_does_not_refresh_retry_or_clean_locally(table,
transport, monkeypatch, target):
+ transport.uuid = 'replacement-table-uuid'
+ transport.status = 400
+
+ def unexpected_cleanup():
+ pytest.fail('Catalog rejection must not trigger local cleanup')
+
+ monkeypatch.setattr(table, 'rollback_helper', unexpected_cleanup)
+
+ with pytest.raises(RuntimeError, match='table UUID mismatch'):
+ table.rollback_to(target)
+
+ assert len(transport.requests) == 1
+ assert transport.requests[0].method == 'POST'
+ assert request_body(transport.requests[0])['tableId'] ==
'loaded-table-uuid'
+
+
+def test_compact_rollback_rejection_preserves_identity(table, transport):
+ transport.status = 400
+ rollback =
CommitRollback(table.catalog_environment.catalog_table_rollback())
+
+ assert not rollback.try_to_rollback(SimpleNamespace(commit_kind='COMPACT',
id=3))
+
+ assert len(transport.requests) == 1
+ assert request_body(transport.requests[0])['tableId'] ==
'loaded-table-uuid'
+ assert request_body(transport.requests[0])['fromSnapshot'] == 3
+
+
[email protected]('table_uuid', [None, 'known-uuid'])
+def test_legacy_catalog_override_never_discards_known_uuid(table_uuid):
+ calls = []
+
+ class LegacyCatalog:
+ def rollback_to(self, identifier, instant, from_snapshot=None):
+ calls.append((identifier, instant, from_snapshot))
+
+ identifier = Identifier('db', 'table')
+ instant = Instant.snapshot(1)
+ if table_uuid is None:
+ TableRollback(LegacyCatalog(), identifier).rollback_to(instant, 2)
+ assert calls == [(identifier, instant, 2)]
+ else:
+ with pytest.raises(RuntimeError) as failure:
+ TableRollback(LegacyCatalog(), identifier,
table_uuid).rollback_to(instant, 2)
+ assert isinstance(failure.value.__cause__, TypeError)
+ assert calls == []
+
+
[email protected]('source,target', [(None, 'dev'), ('dev', None),
('dev', 'release')])
+def test_copied_branch_rollback_omits_source_identity(catalog, transport,
source, target):
+ table = catalog.get_table(Identifier('db', 'table', branch=source))
+ copied = table.copy({'branch': target})
+ transport.requests.clear()
+
+ copied.rollback_to(1)
+
+ assert table.catalog_environment.uuid == 'loaded-table-uuid'
+ assert copied.catalog_environment.uuid is None
+ assert len(transport.requests) == 1
+ assert 'tableId' not in request_body(transport.requests[0])
+ object_name =
unquote_plus(urlsplit(transport.requests[0].url).path.split('/')[-2])
+ assert object_name == Identifier('db', 'table',
branch=target).get_object_name()
+
+
+def test_same_branch_copy_and_independent_load_preserve_identity(table,
catalog, transport):
+ assert table.copy({'scan.parallelism': '2'}).catalog_environment.uuid ==
'loaded-table-uuid'
+ assert table.copy({'branch': 'main'}).catalog_environment.uuid ==
'loaded-table-uuid'
+ assert _rest_catalog_supported(table)
+ assert not _rest_catalog_supported(table.copy({'branch': 'dev'}))
+
+ transport.uuid = 'loaded-dev-uuid'
+ branch = catalog.get_table(Identifier('db', 'table', branch='dev'))
+ assert branch.current_branch() == 'dev'
+ assert branch.catalog_environment.uuid == 'loaded-dev-uuid'
+ assert branch.copy({'branch': 'dev'}).catalog_environment.uuid ==
'loaded-dev-uuid'
+ assert branch.catalog_environment.snapshot_commit(None).uuid ==
'loaded-dev-uuid'
+ assert _rest_catalog_supported(branch)
+ transport.requests.clear()
+
+ branch.rollback_to(1)
+
+ assert len(transport.requests) == 1
+ assert request_body(transport.requests[0])['tableId'] == 'loaded-dev-uuid'
+
+
+def test_copied_branch_rejects_source_read_metadata(table, monkeypatch):
+ monkeypatch.setattr('pypaimon.read.native_plan.native_method_available',
lambda *args: True)
+
+ assert _resolved_rest_table_response(table) ==
table.catalog_environment.rest_table_response
+ assert _resolved_rest_table_response(table.copy({'branch': 'main'})) is
not None
+ assert _resolved_rest_table_response(table.copy({'branch': 'dev'})) is None