Skip to content

Skip metadata loads for table, view, and namespace existence checks - #5438

Open
ayushtkn wants to merge 1 commit into
apache:mainfrom
ayushtkn:existsSkipLoad
Open

Skip metadata loads for table, view, and namespace existence checks#5438
ayushtkn wants to merge 1 commit into
apache:mainfrom
ayushtkn:existsSkipLoad

Conversation

@ayushtkn

@ayushtkn ayushtkn commented Sep 3, 2026

Copy link
Copy Markdown
Member

tableExists, viewExists and namespaceExists each resolve and authorize the entity, then load it in full and discard the result. All three methods return void — the adapter turns a normal return into a 204 with no body — and all three carried a

// TODO: Just skip CatalogHandlers for this one maybe.

Resolution has already done what the endpoint asks. resolveAndAuthorizeBasicTableLikeOperationOrThrow throws notFoundExceptionForTableLikeEntity when the resolved leaf is absent or of the wrong subtype, so everything after it re-establishes a fact already in hand.

  • What a local table or view check cost

loadTable reaches newTableOps(ident).current()doRefresh(), which spends two things per request:

  • A second resolution of the path. doRefresh calls getPassthroughResolvedPath(...), deliberately bypassing the authz resolution set the request just built, so the entity is resolved from the metastore twice.
  • A read and parse of the whole metadata file. refreshFromMetadataLocation(...)TableMetadataParser.read(fileIO, location): an object-storage GET plus a full JSON parse of metadata that is then discarded. loadView is the same shape through ViewMetadataParser.
  • What a local namespace check cost

Much less, stated plainly. LocalIcebergCatalog.loadNamespaceMetadata reads the already-resolved path via getResolvedPath(...) and returns the entity's properties — no storage access, no second resolution. Removing it saves a lookup and a map copy. It is included for consistency across the three endpoints and for the federated benefit below, not for a local performance win.

  • What every federated check saves

Federated catalogs are passthrough, so a Polaris record does not guarantee the entity is still on the remote and the remote must still be consulted. It is now asked whether the entity exists rather than made to load it.

Against a REST-backed catalog that is a HEAD in place of a GET returning full metadata — RESTSessionCatalog.tableExists issues client.head(...). A catalog that does not override exists falls through to the Catalog/ViewCatalog interface default, which loads and catches: the same cost as before, never worse.

Because exists returns false where load threw, the not-found is now raised explicitly — notFoundExceptionForTableLikeEntity for tables and views, noSuchNamespaceException for namespaces — so response statuses for them stay unchanged.

Checklist

  • 🛡️ Don't disclose security issues! (contact security@apache.org)
  • 🔗 Clearly explained why the changes are needed, or linked related issues: Fixes #
  • 🧪 Added/updated tests with good coverage, or manually tested (and explained how)
  • 💡 Added comments for complex logic
  • 🧾 Updated CHANGELOG.md (if needed)
  • 📚 Updated documentation in site/content/in-dev/unreleased (if needed)

Copilot AI lite review requested due to automatic review settings September 3, 2026 11:58
@github-project-automation github-project-automation Bot moved this to PRs In Progress in Basic Kanban Board Sep 3, 2026

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 change is narrowly scoped, preserves error semantics via explicit not-found exceptions, and includes targeted tests for both local and passthrough behaviors.

Pull request overview

This pull request optimizes Iceberg REST “exists” endpoints in IcebergCatalogHandler by avoiding full metadata loads when the request only needs an existence check, while still consulting remote (federated) catalogs for passthrough correctness.

Changes:

  • Update tableExists, viewExists, and namespaceExists to skip load* calls after successful resolution/authorization for local catalogs.
  • For non-local (passthrough/federated) catalogs, call *Exists on the underlying catalog and explicitly throw the appropriate “not found” exception when absent.
  • Add unit tests asserting local “exists” skips loads and that federated “exists” uses *Exists and throws when the remote reports absence.
File summaries
File Description
runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java Avoids expensive metadata loads for existence checks; consults passthrough catalogs via *Exists and preserves not-found semantics.
runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerTest.java Adds coverage ensuring local existence checks don’t load metadata and passthrough existence checks call *Exists and throw when absent.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@vigneshio vigneshio 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.

LGTM..

@flyingImer flyingImer left a comment

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.

No blocking concerns from my side. One non-blocking behavior question inline.

Comment on lines +1327 to +1329
if (!(baseCatalog instanceof LocalIcebergCatalog)
&& !baseCatalog.tableExists(tableIdentifier)) {
throw notFoundExceptionForTableLikeEntity(

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.

Is the change in metadata-table existence semantics intentional? IIRC, with Hadoop federation, tableExists can return true for a synthetic metadata table such as ns.orders.snapshots, while CatalogHandlerUtils.loadTable explicitly rejects BaseMetadataTable. http HEAD method therefore changes from 404 to 204 while GET still returns 404. If intentional, could we cover this case and note the status change in the PR description?

@ayushtkn
ayushtkn force-pushed the existsSkipLoad branch 2 times, most recently from f21d44d to e3a2599 Compare September 8, 2026 23:36
catalogHandlerUtils().loadNamespace(namespaceCatalog, namespace);
if (!(baseCatalog instanceof LocalIcebergCatalog)
&& !namespaceCatalog.namespaceExists(namespace)) {
throw noSuchNamespaceException(namespace);

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.

nit: the logic is correct, but a bit obscure. The fact that we delegate the existence check to namespaceCatalog is not apparent at first glance... It might be easier to read this code it had nested if statements.... with a comment about why we do not have to do the check for local catalog.

// A federated catalog is loaded rather than asked with tableExists, because tableExists
// reports a synthetic metadata table such as ns.orders.snapshots as present while loadTable
// rejects it as not found. Loading keeps the two answers in agreement for those identifiers.
if (!(baseCatalog instanceof LocalIcebergCatalog)) {

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.

Could you add a comment why loading is not needed for local catalogs?

Resolution already establishes existence on a local catalog, so these
HEAD requests no longer load and parse metadata.

Federated catalogs are asked via exists() instead, except for identifiers
that could name a synthetic metadata table. Hadoop and Hive report
ns.orders.snapshots as present -- HadoopCatalog inherits the default
Catalog.tableExists, which loads the table and swallows the rejection,
and HiveCatalog.tableExists rewrites the identifier to the base table
before asking the metastore -- while loadTable rejects it as not found,
so HEAD would answer 204 where GET answers 404.

A metadata table always ends in a MetadataTableType name, and an ordinary
table may carry one of those names too, so only those identifiers keep
the load, which tells the two apart exactly as GET does.
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.

5 participants