Skip to content

REST: Don't URL-encode view names in create_view and register_view bodies - #4151

Merged
kevinjqliu merged 6 commits into
apache:mainfrom
kevinjqliu:kevinjqliu-fix-rest-view-name-encoding
Oct 25, 2026
Merged

kevinjqliu merged 6 commits into
apache:mainfrom
kevinjqliu:kevinjqliu-fix-rest-view-name-encoding

Conversation

@kevinjqliu

@kevinjqliu kevinjqliu commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Rationale for this change

Same kind of bug as #4112, but for views. create_view and register_view put the URL-encoded view name in the JSON body, so my view gets created as my%20view. Use the raw name instead, like create_table and register_table.

Also added an integration test that round-trips names with spaces, +&=, and non-ASCII through the REST fixture.

Are these changes tested?

Yes, both fail on main:

  • pytest tests/catalog/test_rest.py: passed
  • pytest tests/integration/test_catalog.py -m integration -k test_rest_identifiers_with_special_characters: passed

Are there any user-facing changes?

Yes, bug fix for view names that need URL encoding.

AI Disclosure: Assisted by GitHub Copilot.



@pytest.mark.integration
def test_rest_identifiers_with_special_characters(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

integration test that checks special character handling on relevant endpoints


request = CreateViewRequest(
name=namespace_and_view["view"],
name=self._identifier_to_validated_tuple(identifier)[-1],

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use _identifier_to_validated_tuple for request bodies so names aren't escaped, and escape only in URL paths.

mirrors CreateTableRequest

request = CreateTableRequest(
name=self._identifier_to_validated_tuple(identifier)[-1],
location=location,
table_schema=fresh_schema,
partition_spec=fresh_partition_spec,
write_order=fresh_sort_order,
stage_create=stage_create,
properties=properties,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idea (not necessarily for this PR): what if Identifier still acted like a tuple but had named methods on it as well? I really don't like the -1 here, since it's not always intuitive why we're doing that.

Could you add a comment explaining what you're trying to do?

Comment on lines +1828 to +1835
identifier_tuple = self._identifier_to_validated_tuple(identifier)
if self.table_exists(identifier):
raise TableAlreadyExistsError(f"Table {namespace}.{view} already exists")
raise TableAlreadyExistsError(f"Table {'.'.join(identifier_tuple)} already exists")

request = RegisterViewRequest(name=view, metadata_location=metadata_location)
request = RegisterViewRequest(name=identifier_tuple[-1], metadata_location=metadata_location)
serialized_json = request.model_dump_json().encode(UTF8)
response = self._session.post(
self.url(Endpoints.register_view, namespace=namespace),
self.url(Endpoints.register_view, namespace=namespace_and_view["namespace"]),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

follows register_table

namespace_and_table = self._split_identifier_for_path(identifier)
request = RegisterTableRequest(
name=self._identifier_to_validated_tuple(identifier)[-1],
metadata_location=metadata_location,
overwrite=overwrite,
)
serialized_json = request.model_dump_json().encode(UTF8)
response = self._session.post(
self.url(Endpoints.register_table, namespace=namespace_and_table["namespace"]),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bug was using the wrong helper in the request body. The body should not be escaped, the URL should be. We should probably do some refactoring here (esp. with the naming).

Helper Escaped? Returns for ("ns", "my view") Use for
_identifier_to_validated_tuple(identifier) No ("ns", "my view") JSON body, error messages. Raises NoSuchIdentifierError if there's no namespace
_split_identifier_for_path(identifier, kind) Yes {"namespace": "ns", "view": "my%20view"} URL path only

@kevinjqliu kevinjqliu changed the title REST: Send raw view name in create_view and register_view REST: Don't URL-encode view names in create_view and register_view bodies Oct 25, 2026
@kevinjqliu
kevinjqliu marked this pull request as ready for review October 25, 2026 00:46
Copilot AI balanced review requested due to automatic review settings October 25, 2026 00:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused fix preserves path encoding, covers both affected request bodies with regression tests, and has no identified blocking issues.

No open issues 🎉

What changed in this PR

Fixes REST view creation and registration so names containing special characters remain unchanged in JSON request bodies, while URL paths retain encoding.

Changes:

  • Uses raw view names in create_view and register_view payloads.
  • Adds regression tests for both payloads and integration coverage for special-character identifiers.
File Description
tests/​integration/​test_catalog.py Adds REST round-trip checks for namespaces, tables, and views with special characters.
tests/​catalog/​test_rest.py Verifies create and register view requests preserve raw names in JSON bodies.
pyiceberg/​catalog/​rest/​__init__.py Separates raw view names from encoded paths and uses readable identifiers in conflict errors.

Review effort: Balanced

Comment thread tests/integration/test_catalog.py Outdated
rest_catalog.create_view(view_identifier, view.schema(), view.current_version())
assert rest_catalog.view_exists(view_identifier)
assert rest_catalog.list_views(namespace) == [view_identifier]
# TODO: Cover register_view once View exposes its metadata location (#4088)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should add register_view here, but its not yet merged

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added register_view here after rebasing #4088

kevinjqliu and others added 6 commits October 24, 2026 20:49
create_view and register_view took the view name from
_split_identifier_for_path, which URL-quotes it for use as a path
segment, and put it in the JSON request body. A view named "my view"
was created as "my%20view". Use the raw name, as create_table and
register_table already do.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Round-trip namespace, table, and view names containing spaces, reserved
URL characters, and non-ASCII text through path, query, and body
parameters. This catches encoding mistakes like the double-encoded
list_namespaces parent and the encoded view name in create_view.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
commit_table is the only caller of the TableIdentifier branch of
_split_identifier_for_path, so table updates had no coverage for names
that need URL encoding.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Matches create_view and register_table: path from _split_identifier_for_path, raw name for the body.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Register the created view under a second name that needs URL encoding,
now that View exposes its metadata location (apache#4088).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@kevinjqliu
kevinjqliu force-pushed the kevinjqliu-fix-rest-view-name-encoding branch from acc4f8c to 6af5063 Compare October 25, 2026 03:52
assert created_view.metadata_location is not None
rest_catalog.register_view(registered_view_identifier, created_view.metadata_location)
assert rest_catalog.view_exists(registered_view_identifier)
assert sorted(rest_catalog.list_views(namespace)) == sorted([view_identifier, registered_view_identifier])

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

two views here because one is from create_view, another from register_review

@kevinjqliu
kevinjqliu requested review from Fokko and geruh October 25, 2026 03:55
@kevinjqliu
kevinjqliu enabled auto-merge October 25, 2026 04:05
@kevinjqliu
kevinjqliu added this pull request to the merge queue Oct 25, 2026
Merged via the queue into apache:main with commit c8502b3 Oct 25, 2026
21 checks passed
@kevinjqliu
kevinjqliu deleted the kevinjqliu-fix-rest-view-name-encoding branch October 25, 2026 04:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants