Skip to content

[Storage] Add UC Delta Rest Catalog API loadTable client APIs#6659

Open
TimothyW553 wants to merge 1 commit into
delta-io:masterfrom
TimothyW553:stack/drc-loadtable-storage
Open

[Storage] Add UC Delta Rest Catalog API loadTable client APIs#6659
TimothyW553 wants to merge 1 commit into
delta-io:masterfrom
TimothyW553:stack/drc-loadtable-storage

Conversation

@TimothyW553
Copy link
Copy Markdown
Collaborator

@TimothyW553 TimothyW553 commented Apr 23, 2026

🥞 Stacked PR

Use this link to review incremental changes.


Which Delta project/connector is this regarding?

Storage / Unity Catalog

Description

This PR adds the storage-layer client support needed for named-table loadTable through the UC Delta Rest Catalog API.

Without this change, Delta has no common storage-level client API for UC Delta Rest Catalog API named-table metadata and credentials. The Spark catalog wiring is added in the next PR.

This PR adds:

  • UCClient.supportsUCDeltaRestCatalogApi(), loadTable(...), and getTableCredentials(...).
  • UCDeltaClient and UCDeltaTokenBasedRestClient as the UC Delta Rest Catalog API surface below the existing UC client.
  • UCTokenBasedRestClient probing of the UC Delta Rest Catalog API config endpoint.
  • Named table metadata and table credential calls when the UC server advertises the required endpoints.
  • Loud default failures for legacy-only UC clients, so UC Delta Rest Catalog API operations cannot silently route through unsupported clients.
  • UC SDK schema conversion inside the token-based client, so UC SDK schema types do not leak past the storage client boundary.
  • UC_DELTA_REST_CATALOG_API_ENABLED for Spark UC integration tests; unset means enabled by default, and false runs the legacy UC Spark catalog path.

How was this patch tested?

Covered by UCTokenBasedRestClientSuite for supported endpoints, missing endpoints, config probe failures, default unsupported client behavior, metadata conversion, and table credential handling.

Covered by UnityCatalogSupportTest for the integration-test environment variable behavior.

Local verification:

./build/sbt "storage/testOnly io.delta.storage.commit.uccommitcoordinator.UCTokenBasedRestClientSuite"

Does this PR introduce any user-facing changes?

No. This adds internal client APIs and an integration-test toggle.

@TimothyW553
Copy link
Copy Markdown
Collaborator Author

Range-diff: master (17be210 -> 4610c42)
docs/superpowers/specs/2026-04-22-drc-loadtable-design.md
@@ -0,0 +1,213 @@
+diff --git a/docs/superpowers/specs/2026-04-22-drc-loadtable-design.md b/docs/superpowers/specs/2026-04-22-drc-loadtable-design.md
+new file mode 100644
+--- /dev/null
++++ b/docs/superpowers/specs/2026-04-22-drc-loadtable-design.md
++# DRC LoadTable First Slice Design
++
++## Goal
++
++Implement the first Delta-side slice of Delta REST Catalog (DRC) integration for Unity Catalog:
++
++- DRC-backed `loadTable`
++- DRC-backed `getTableCredentials`
++
++This slice is intentionally read-only. It does not implement DRC `createTable`, DRC commit, or DRC get-commits behavior yet.
++
++## Authority and Scope
++
++This design is grounded in:
++
++- `delta_rest_catalog_design_doc_clarifications.md`
++- `delta_rest_catalog_api_spec.md`
++- `delta_rest_catalog_provider_design.md`
++- the current generated UC SDK under `unitycatalog/clients/java/target/src/main/java/io/unitycatalog/client/delta/`
++
++The old Delta POC PR [`delta-io/delta#6575`](https://github.com/delta-io/delta/pull/6575) is used only for shape:
++
++- `UCDeltaClient`
++- `DRCMetadataAdapter`
++- `DeltaRestSchemaConverter`
++- `AbstractDeltaCatalog.loadTable` wiring
++
++Any conflict between the POC and the current docs / SDK is resolved in favor of the docs and the current SDK.
++
++## Confirmed Decisions
++
++- Delta builds directly against UC master at a pinned SHA. No compile-time DRC shim architecture.
++- `UCDeltaClient` extends the existing `UCClient`.
++- The DRC read-side surface should mirror the server contract and return raw UC SDK types.
++- For this slice, the new read-side methods are name-based:
++  - `loadTable(catalog, schema, table)`
++  - `getTableCredentials(catalog, schema, table, operation)`
++- `tableId` is not an input to the read-side methods. It is returned by `LoadTableResponse.metadata.table-uuid`.
++- If the DRC path is unavailable or any DRC load/credential call fails, Delta must log a WARN and fall back to the legacy UC API path.
++- `UCDeltaClientProvider` is deferred. It belongs to the later shared-client / commit-coordinator slice, not this first load-only slice.
++
++## Contract Clarification: Credentials Are Separate
++
++The current local spec and generated SDK do not include credentials in `LoadTableResponse`.
++
++- The prose spec explicitly says credential vending is not part of `loadTable`.
++- The generated `LoadTableResponse` model contains only:
++  - `metadata`
++  - `commits`
++  - `uniform`
++  - `latest-table-version`
++
++Therefore, Delta must treat DRC table loading as two distinct RPCs:
++
++1. `TablesApi.loadTable(catalog, schema, table)`
++2. `TemporaryCredentialsApi.getTableCredentials(READ, catalog, schema, table)`
++
++## Recommended Architecture
++
++### 1. Delta-side client
++
++Add a Delta-side `UCDeltaClient` interface in storage that extends `UCClient`.
++
++For this first slice, it will expose:
++
++- legacy `UCClient` methods unchanged
++- new DRC read-side methods returning raw SDK types:
++  - `io.unitycatalog.client.delta.model.LoadTableResponse loadTable(...)`
++  - `io.unitycatalog.client.delta.model.CredentialsResponse getTableCredentials(...)`
++
++It will also define future DRC-shaped write methods now so the interface grows in the right direction:
++
++- DRC `createTable(...)`
++- DRC name-based `commit(...)`
++
++Those future methods should include the data the update path will need later:
++
++- `oldMetadata`
++- `newMetadata`
++- `oldProtocol`
++- `newProtocol`
++- optional `etag`
++- optional `uniform`
++
++For this slice, those future write methods throw immediately with a clear unsupported message.
++
++### 2. Concrete implementation
++
++Implement one concrete `UCDeltaClient` against the current UC client jars.
++
++It should:
++
++- reuse the UC-owned `ApiClient`
++- construct DRC `TablesApi` and `TemporaryCredentialsApi` from that `ApiClient`
++- use the existing legacy `UCClient` behavior as fallback
++
++This is not a stop-the-world refactor. The existing path-based legacy APIs remain intact.
++
++### 3. Catalog wiring
++
++`AbstractDeltaCatalog.loadTable` is the first DRC consumer.
++
++When the runtime DRC flag is enabled:
++
++- inspect `delegate`
++- if it implements `io.unitycatalog.client.delta.DeltaRestClientProvider`, first check `getDeltaTablesApi()`
++- if `getDeltaTablesApi()` is empty, treat DRC as unavailable and fall back
++- otherwise obtain the shared UC `ApiClient`
++- build or reuse a `UCDeltaClient`
++- attempt DRC `loadTable`
++- attempt DRC `getTableCredentials(..., READ)`
++- convert the DRC schema directly to Spark schema
++- build the Delta table using the DRC metadata and READ credentials
++
++If any part of that DRC path fails, log:
++
++- `WARN: falling back to legacy UC API`
++
++and continue via the existing legacy `super.loadTable(...)` path.
++
++Fallback policy should stay simple:
++
++- if the runtime DRC flag is off, use the legacy path
++- if DRC is not available through `DeltaRestClientProvider`, use the legacy path
++- if the attempted DRC load path fails, log the WARN and use the legacy path
++
++The implementation should not try to encode a large taxonomy of fallback cases in the design or in control flow.
++
++## Metadata and Schema Adaptation
++
++### `DRCMetadataAdapter`
++
++Add a Delta-side adapter around the UC SDK `TableMetadata` implementing `AbstractMetadata`.
++
++Purpose:
++
++- expose Delta metadata through the storage abstraction
++- allow DRC-aware consumers to access the UC schema object directly
++- keep lazy `getSchemaString()` only as fallback / compatibility support
++
++This follows the useful shape of the POC while matching the current SDK, not the older SDK shape.
++
++### `DeltaRestSchemaConverter`
++
++Add a converter from the current UC SDK DRC schema model to Spark `StructType`.
++
++Requirements:
++
++- no JSON roundtrip on the happy path
++- support nested struct / array / map / decimal types
++- preserve field nullability
++- preserve field metadata needed by Delta and Spark, especially column mapping metadata
++
++The POC converter is the right idea but not authoritative implementation.
++
++## Behavior of Unsupported Methods in This Slice
++
++The following DRC methods may exist on `UCDeltaClient` now but should throw for this slice:
++
++- DRC `createTable`
++- DRC name-based `commit`
++- DRC name-based `getCommits` if added
++
++This keeps the interface shape aligned with the later design without accidentally implying write support exists.
++
++## File Impact
++
++Expected initial touch points:
++
++- `storage/.../uccommitcoordinator/UCDeltaClient.java`
++- `storage/.../uccommitcoordinator/` concrete implementation
++- `storage/.../uccommitcoordinator/DRCMetadataAdapter.java`
++- `spark/.../catalog/AbstractDeltaCatalog.scala`
++- `spark/.../catalog/DeltaRestSchemaConverter.scala`
++
++Explicitly not in this slice:
++
++- `UCCommitCoordinatorBuilder`
++- `CatalogTrackedInfo`
++- `SnapshotManagement`
++- `OptimisticTransaction`
++- kill switch weakening
++- DRC create / commit logic
++
++## Testing
++
++Required tests for this slice:
++
++- schema converter unit tests for primitive, decimal, array, map, and nested struct cases
++- field metadata preservation tests, especially column mapping metadata
++- successful DRC `loadTable` path
++- successful DRC `getTableCredentials(READ)` path
++- fallback when provider is absent
++- fallback when provider exposes no DRC support
++- fallback when DRC `loadTable` throws
++- fallback when DRC credential vending throws
++- WARN log assertion on fallback
++
++## Non-Goals
++
++This slice does not:
++
++- implement DRC `createTable`
++- implement DRC commit or update-table behavior
++- plumb etag through snapshot or commit state
++- share a Delta-owned `UCDeltaClient` instance through `UCDeltaClientProvider`
++- weaken UC-managed metadata kill switches
++
++Those belong to later slices.
\ No newline at end of file
storage/src/test/scala/io/delta/storage/commit/uccommitcoordinator/UCTokenBasedRestClientSuite.scala
@@ -0,0 +1,102 @@
+diff --git a/storage/src/test/scala/io/delta/storage/commit/uccommitcoordinator/UCTokenBasedRestClientSuite.scala b/storage/src/test/scala/io/delta/storage/commit/uccommitcoordinator/UCTokenBasedRestClientSuite.scala
+--- a/storage/src/test/scala/io/delta/storage/commit/uccommitcoordinator/UCTokenBasedRestClientSuite.scala
++++ b/storage/src/test/scala/io/delta/storage/commit/uccommitcoordinator/UCTokenBasedRestClientSuite.scala
+ import io.delta.storage.commit.{Commit, CommitFailedException}
+ import io.delta.storage.commit.actions.AbstractMetadata
+ import io.delta.storage.commit.uniform.{IcebergMetadata, UniformMetadata}
++import io.unitycatalog.client.ApiClientBuilder
+ import io.unitycatalog.client.auth.TokenProvider
+ 
+ import org.apache.hadoop.fs.{FileStatus, Path}
+   private var serverUri: String = _
+   private var metastoreHandler: HttpExchange => Unit = _
+   private var commitsHandler: HttpExchange => Unit = _
++  private var legacyTablesHandler: HttpExchange => Unit = _
+   private val objectMapper = new ObjectMapper()
+ 
+   override def beforeAll(): Unit = {
+       }
+       exchange.close()
+     })
++    server.createContext("/api/2.1/unity-catalog/tables", exchange => {
++      if (legacyTablesHandler != null) legacyTablesHandler(exchange)
++      else sendJson(exchange, HttpStatus.SC_NOT_FOUND, "{}")
++      exchange.close()
++    })
+     server.start()
+     serverUri = s"http://localhost:${server.getAddress.getPort}"
+   }
+   override def beforeEach(): Unit = {
+     metastoreHandler = null
+     commitsHandler = null
++    legacyTablesHandler = null
+   }
+ 
+   private def readRequestBody(exchange: HttpExchange): String = {
+   private def createClient(): UCTokenBasedRestClient =
+     new UCTokenBasedRestClient(serverUri, createTokenProvider(), Collections.emptyMap())
+ 
++  private def createLegacyClient(): UCTokenBasedRestClient = {
++    val apiClient = ApiClientBuilder.create()
++      .uri(serverUri)
++      .tokenProvider(createTokenProvider())
++      .build()
++    new UCTokenBasedRestClient(apiClient, Optional.empty())
++  }
++
+   private def withClient(fn: UCTokenBasedRestClient => Unit): Unit = {
+     val client = createClient()
+     try fn(client) finally client.close()
+   }
+ 
++  private def withLegacyClient(fn: UCTokenBasedRestClient => Unit): Unit = {
++    val client = createLegacyClient()
++    try fn(client) finally client.close()
++  }
++
+   private def createCommit(version: Long): Commit = {
+     val fs = new FileStatus(1024L, false, 1, 4096L, System.currentTimeMillis(),
+       new Path(s"/path/_delta_log/_staged_commits/$version.uuid.json"))
+     }
+   }
+ 
++  test("loadTable falls back to legacy UC API and converts table metadata") {
++    legacyTablesHandler = exchange => {
++      assert(exchange.getRequestMethod === "GET")
++      sendJson(
++        exchange,
++        HttpStatus.SC_OK,
++        """{
++          |  "name": "tbl",
++          |  "catalog_name": "main",
++          |  "schema_name": "default",
++          |  "table_id": "11111111-1111-1111-1111-111111111111",
++          |  "table_type": "MANAGED",
++          |  "data_source_format": "DELTA",
++          |  "storage_location": "s3://bucket/path/to/table",
++          |  "created_at": 10,
++          |  "updated_at": 11,
++          |  "properties": {"delta.appendOnly":"true"},
++          |  "columns": [
++          |    {"name":"value","type_text":"string","nullable":true,"position":1},
++          |    {"name":"region","type_text":"string","nullable":false,"position":2,"partition_index":1},
++          |    {"name":"date","type_text":"date","nullable":false,"position":3,"partition_index":0}
++          |  ]
++          |}""".stripMargin)
++    }
++
++    withLegacyClient { client =>
++      val response = client.loadTable("main", "default", "tbl")
++      val metadata = response.getMetadata
++      assert(metadata.getLocation === "s3://bucket/path/to/table")
++      assert(metadata.getPartitionColumns === java.util.Arrays.asList("date", "region"))
++      assert(metadata.getProperties.get("delta.appendOnly") === "true")
++      assert(metadata.getColumns.getFields.size() === 3)
++      assert(metadata.getColumns.getFields.get(1).getName === "region")
++      assert(!metadata.getColumns.getFields.get(1).getNullable)
++    }
++  }
++
+   // commit tests
+   test("commit succeeds with valid parameters") {
+     withClient { client =>
\ No newline at end of file

Reproduce locally: git range-diff 5f1e465..17be210 5f1e465..4610c42 | Disable: git config gitstack.push-range-diff false

@TimothyW553 TimothyW553 force-pushed the stack/drc-loadtable-storage branch from 4610c42 to 5c40a3a Compare April 23, 2026 07:26
@TimothyW553
Copy link
Copy Markdown
Collaborator Author

Range-diff: master (4610c42 -> 5c40a3a)
docs/superpowers/specs/2026-04-22-drc-loadtable-design.md
@@ -0,0 +1,213 @@
+diff --git a/docs/superpowers/specs/2026-04-22-drc-loadtable-design.md b/docs/superpowers/specs/2026-04-22-drc-loadtable-design.md
+new file mode 100644
+--- /dev/null
++++ b/docs/superpowers/specs/2026-04-22-drc-loadtable-design.md
++# DRC LoadTable First Slice Design
++
++## Goal
++
++Implement the first Delta-side slice of Delta REST Catalog (DRC) integration for Unity Catalog:
++
++- DRC-backed `loadTable`
++- DRC-backed `getTableCredentials`
++
++This slice is intentionally read-only. It does not implement DRC `createTable`, DRC commit, or DRC get-commits behavior yet.
++
++## Authority and Scope
++
++This design is grounded in:
++
++- `delta_rest_catalog_design_doc_clarifications.md`
++- `delta_rest_catalog_api_spec.md`
++- `delta_rest_catalog_provider_design.md`
++- the current generated UC SDK under `unitycatalog/clients/java/target/src/main/java/io/unitycatalog/client/delta/`
++
++The old Delta POC PR [`delta-io/delta#6575`](https://github.com/delta-io/delta/pull/6575) is used only for shape:
++
++- `UCDeltaClient`
++- `DRCMetadataAdapter`
++- `DeltaRestSchemaConverter`
++- `AbstractDeltaCatalog.loadTable` wiring
++
++Any conflict between the POC and the current docs / SDK is resolved in favor of the docs and the current SDK.
++
++## Confirmed Decisions
++
++- Delta builds directly against UC master at a pinned SHA. No compile-time DRC shim architecture.
++- `UCDeltaClient` extends the existing `UCClient`.
++- The DRC read-side surface should mirror the server contract and return raw UC SDK types.
++- For this slice, the new read-side methods are name-based:
++  - `loadTable(catalog, schema, table)`
++  - `getTableCredentials(catalog, schema, table, operation)`
++- `tableId` is not an input to the read-side methods. It is returned by `LoadTableResponse.metadata.table-uuid`.
++- If the DRC path is unavailable or any DRC load/credential call fails, Delta must log a WARN and fall back to the legacy UC API path.
++- `UCDeltaClientProvider` is deferred. It belongs to the later shared-client / commit-coordinator slice, not this first load-only slice.
++
++## Contract Clarification: Credentials Are Separate
++
++The current local spec and generated SDK do not include credentials in `LoadTableResponse`.
++
++- The prose spec explicitly says credential vending is not part of `loadTable`.
++- The generated `LoadTableResponse` model contains only:
++  - `metadata`
++  - `commits`
++  - `uniform`
++  - `latest-table-version`
++
++Therefore, Delta must treat DRC table loading as two distinct RPCs:
++
++1. `TablesApi.loadTable(catalog, schema, table)`
++2. `TemporaryCredentialsApi.getTableCredentials(READ, catalog, schema, table)`
++
++## Recommended Architecture
++
++### 1. Delta-side client
++
++Add a Delta-side `UCDeltaClient` interface in storage that extends `UCClient`.
++
++For this first slice, it will expose:
++
++- legacy `UCClient` methods unchanged
++- new DRC read-side methods returning raw SDK types:
++  - `io.unitycatalog.client.delta.model.LoadTableResponse loadTable(...)`
++  - `io.unitycatalog.client.delta.model.CredentialsResponse getTableCredentials(...)`
++
++It will also define future DRC-shaped write methods now so the interface grows in the right direction:
++
++- DRC `createTable(...)`
++- DRC name-based `commit(...)`
++
++Those future methods should include the data the update path will need later:
++
++- `oldMetadata`
++- `newMetadata`
++- `oldProtocol`
++- `newProtocol`
++- optional `etag`
++- optional `uniform`
++
++For this slice, those future write methods throw immediately with a clear unsupported message.
++
++### 2. Concrete implementation
++
++Implement one concrete `UCDeltaClient` against the current UC client jars.
++
++It should:
++
++- reuse the UC-owned `ApiClient`
++- construct DRC `TablesApi` and `TemporaryCredentialsApi` from that `ApiClient`
++- use the existing legacy `UCClient` behavior as fallback
++
++This is not a stop-the-world refactor. The existing path-based legacy APIs remain intact.
++
++### 3. Catalog wiring
++
++`AbstractDeltaCatalog.loadTable` is the first DRC consumer.
++
++When the runtime DRC flag is enabled:
++
++- inspect `delegate`
++- if it implements `io.unitycatalog.client.delta.DeltaRestClientProvider`, first check `getDeltaTablesApi()`
++- if `getDeltaTablesApi()` is empty, treat DRC as unavailable and fall back
++- otherwise obtain the shared UC `ApiClient`
++- build or reuse a `UCDeltaClient`
++- attempt DRC `loadTable`
++- attempt DRC `getTableCredentials(..., READ)`
++- convert the DRC schema directly to Spark schema
++- build the Delta table using the DRC metadata and READ credentials
++
++If any part of that DRC path fails, log:
++
++- `WARN: falling back to legacy UC API`
++
++and continue via the existing legacy `super.loadTable(...)` path.
++
++Fallback policy should stay simple:
++
++- if the runtime DRC flag is off, use the legacy path
++- if DRC is not available through `DeltaRestClientProvider`, use the legacy path
++- if the attempted DRC load path fails, log the WARN and use the legacy path
++
++The implementation should not try to encode a large taxonomy of fallback cases in the design or in control flow.
++
++## Metadata and Schema Adaptation
++
++### `DRCMetadataAdapter`
++
++Add a Delta-side adapter around the UC SDK `TableMetadata` implementing `AbstractMetadata`.
++
++Purpose:
++
++- expose Delta metadata through the storage abstraction
++- allow DRC-aware consumers to access the UC schema object directly
++- keep lazy `getSchemaString()` only as fallback / compatibility support
++
++This follows the useful shape of the POC while matching the current SDK, not the older SDK shape.
++
++### `DeltaRestSchemaConverter`
++
++Add a converter from the current UC SDK DRC schema model to Spark `StructType`.
++
++Requirements:
++
++- no JSON roundtrip on the happy path
++- support nested struct / array / map / decimal types
++- preserve field nullability
++- preserve field metadata needed by Delta and Spark, especially column mapping metadata
++
++The POC converter is the right idea but not authoritative implementation.
++
++## Behavior of Unsupported Methods in This Slice
++
++The following DRC methods may exist on `UCDeltaClient` now but should throw for this slice:
++
++- DRC `createTable`
++- DRC name-based `commit`
++- DRC name-based `getCommits` if added
++
++This keeps the interface shape aligned with the later design without accidentally implying write support exists.
++
++## File Impact
++
++Expected initial touch points:
++
++- `storage/.../uccommitcoordinator/UCDeltaClient.java`
++- `storage/.../uccommitcoordinator/` concrete implementation
++- `storage/.../uccommitcoordinator/DRCMetadataAdapter.java`
++- `spark/.../catalog/AbstractDeltaCatalog.scala`
++- `spark/.../catalog/DeltaRestSchemaConverter.scala`
++
++Explicitly not in this slice:
++
++- `UCCommitCoordinatorBuilder`
++- `CatalogTrackedInfo`
++- `SnapshotManagement`
++- `OptimisticTransaction`
++- kill switch weakening
++- DRC create / commit logic
++
++## Testing
++
++Required tests for this slice:
++
++- schema converter unit tests for primitive, decimal, array, map, and nested struct cases
++- field metadata preservation tests, especially column mapping metadata
++- successful DRC `loadTable` path
++- successful DRC `getTableCredentials(READ)` path
++- fallback when provider is absent
++- fallback when provider exposes no DRC support
++- fallback when DRC `loadTable` throws
++- fallback when DRC credential vending throws
++- WARN log assertion on fallback
++
++## Non-Goals
++
++This slice does not:
++
++- implement DRC `createTable`
++- implement DRC commit or update-table behavior
++- plumb etag through snapshot or commit state
++- share a Delta-owned `UCDeltaClient` instance through `UCDeltaClientProvider`
++- weaken UC-managed metadata kill switches
++
++Those belong to later slices.
\ No newline at end of file
storage/src/test/scala/io/delta/storage/commit/uccommitcoordinator/UCTokenBasedRestClientSuite.scala
@@ -6,8 +6,10 @@
  import io.delta.storage.commit.uniform.{IcebergMetadata, UniformMetadata}
 +import io.unitycatalog.client.ApiClientBuilder
  import io.unitycatalog.client.auth.TokenProvider
++import io.unitycatalog.client.delta.model
  
  import org.apache.hadoop.fs.{FileStatus, Path}
+ import org.apache.http.HttpStatus
    private var serverUri: String = _
    private var metastoreHandler: HttpExchange => Unit = _
    private var commitsHandler: HttpExchange => Unit = _
@@ -97,6 +99,51 @@
 +    }
 +  }
 +
++  test("loadTable falls back to legacy UC API and parses type_json schemas") {
++    legacyTablesHandler = exchange => {
++      assert(exchange.getRequestMethod === "GET")
++      sendJson(
++        exchange,
++        HttpStatus.SC_OK,
++        """{
++          |  "name": "tbl",
++          |  "catalog_name": "main",
++          |  "schema_name": "default",
++          |  "table_id": "11111111-1111-1111-1111-111111111111",
++          |  "table_type": "MANAGED",
++          |  "data_source_format": "DELTA",
++          |  "storage_location": "s3://bucket/path/to/table",
++          |  "created_at": 10,
++          |  "updated_at": 11,
++          |  "columns": [
++          |    {
++          |      "name":"payload",
++          |      "nullable":false,
++          |      "position":0,
++          |      "type_json":"{\"type\":\"struct\",\"fields\":[{\"name\":\"tags\",\"type\":{\"type\":\"array\",\"element-type\":\"string\",\"contains-null\":true},\"nullable\":true,\"metadata\":{\"comment\":\"nested tags\"}}]}"
++          |    }
++          |  ]
++          |}""".stripMargin)
++    }
++
++    withLegacyClient { client =>
++      val response = client.loadTable("main", "default", "tbl")
++      val payloadField = response.getMetadata.getColumns.getFields.get(0)
++      val payloadType = payloadField.getType.asInstanceOf[model.StructType]
++      val nestedField = payloadType.getFields.get(0)
++      val nestedType = nestedField.getType.asInstanceOf[model.ArrayType]
++
++      assert(payloadField.getName === "payload")
++      assert(!payloadField.getNullable)
++      assert(nestedField.getName === "tags")
++      assert(nestedField.getMetadata.get("comment") === "nested tags")
++      assert(nestedType.getElementType.isInstanceOf[model.PrimitiveType])
++      assert(nestedType.getContainsNull)
++      assert(
++        nestedType.getElementType.asInstanceOf[model.PrimitiveType].getType === "string")
++    }
++  }
++
    // commit tests
    test("commit succeeds with valid parameters") {
      withClient { client =>
\ No newline at end of file

Reproduce locally: git range-diff dc0340b..4610c42 5f1e465..5c40a3a | Disable: git config gitstack.push-range-diff false

@TimothyW553 TimothyW553 force-pushed the stack/drc-loadtable-storage branch from 5c40a3a to 8361784 Compare April 23, 2026 07:34
@TimothyW553
Copy link
Copy Markdown
Collaborator Author

Range-diff: master (5c40a3a -> 8361784)
docs/superpowers/specs/2026-04-22-drc-loadtable-design.md
@@ -0,0 +1,213 @@
+diff --git a/docs/superpowers/specs/2026-04-22-drc-loadtable-design.md b/docs/superpowers/specs/2026-04-22-drc-loadtable-design.md
+new file mode 100644
+--- /dev/null
++++ b/docs/superpowers/specs/2026-04-22-drc-loadtable-design.md
++# DRC LoadTable First Slice Design
++
++## Goal
++
++Implement the first Delta-side slice of Delta REST Catalog (DRC) integration for Unity Catalog:
++
++- DRC-backed `loadTable`
++- DRC-backed `getTableCredentials`
++
++This slice is intentionally read-only. It does not implement DRC `createTable`, DRC commit, or DRC get-commits behavior yet.
++
++## Authority and Scope
++
++This design is grounded in:
++
++- `delta_rest_catalog_design_doc_clarifications.md`
++- `delta_rest_catalog_api_spec.md`
++- `delta_rest_catalog_provider_design.md`
++- the current generated UC SDK under `unitycatalog/clients/java/target/src/main/java/io/unitycatalog/client/delta/`
++
++The old Delta POC PR [`delta-io/delta#6575`](https://github.com/delta-io/delta/pull/6575) is used only for shape:
++
++- `UCDeltaClient`
++- `DRCMetadataAdapter`
++- `DeltaRestSchemaConverter`
++- `AbstractDeltaCatalog.loadTable` wiring
++
++Any conflict between the POC and the current docs / SDK is resolved in favor of the docs and the current SDK.
++
++## Confirmed Decisions
++
++- Delta builds directly against UC master at a pinned SHA. No compile-time DRC shim architecture.
++- `UCDeltaClient` extends the existing `UCClient`.
++- The DRC read-side surface should mirror the server contract and return raw UC SDK types.
++- For this slice, the new read-side methods are name-based:
++  - `loadTable(catalog, schema, table)`
++  - `getTableCredentials(catalog, schema, table, operation)`
++- `tableId` is not an input to the read-side methods. It is returned by `LoadTableResponse.metadata.table-uuid`.
++- If the DRC path is unavailable or any DRC load/credential call fails, Delta must log a WARN and fall back to the legacy UC API path.
++- `UCDeltaClientProvider` is deferred. It belongs to the later shared-client / commit-coordinator slice, not this first load-only slice.
++
++## Contract Clarification: Credentials Are Separate
++
++The current local spec and generated SDK do not include credentials in `LoadTableResponse`.
++
++- The prose spec explicitly says credential vending is not part of `loadTable`.
++- The generated `LoadTableResponse` model contains only:
++  - `metadata`
++  - `commits`
++  - `uniform`
++  - `latest-table-version`
++
++Therefore, Delta must treat DRC table loading as two distinct RPCs:
++
++1. `TablesApi.loadTable(catalog, schema, table)`
++2. `TemporaryCredentialsApi.getTableCredentials(READ, catalog, schema, table)`
++
++## Recommended Architecture
++
++### 1. Delta-side client
++
++Add a Delta-side `UCDeltaClient` interface in storage that extends `UCClient`.
++
++For this first slice, it will expose:
++
++- legacy `UCClient` methods unchanged
++- new DRC read-side methods returning raw SDK types:
++  - `io.unitycatalog.client.delta.model.LoadTableResponse loadTable(...)`
++  - `io.unitycatalog.client.delta.model.CredentialsResponse getTableCredentials(...)`
++
++It will also define future DRC-shaped write methods now so the interface grows in the right direction:
++
++- DRC `createTable(...)`
++- DRC name-based `commit(...)`
++
++Those future methods should include the data the update path will need later:
++
++- `oldMetadata`
++- `newMetadata`
++- `oldProtocol`
++- `newProtocol`
++- optional `etag`
++- optional `uniform`
++
++For this slice, those future write methods throw immediately with a clear unsupported message.
++
++### 2. Concrete implementation
++
++Implement one concrete `UCDeltaClient` against the current UC client jars.
++
++It should:
++
++- reuse the UC-owned `ApiClient`
++- construct DRC `TablesApi` and `TemporaryCredentialsApi` from that `ApiClient`
++- use the existing legacy `UCClient` behavior as fallback
++
++This is not a stop-the-world refactor. The existing path-based legacy APIs remain intact.
++
++### 3. Catalog wiring
++
++`AbstractDeltaCatalog.loadTable` is the first DRC consumer.
++
++When the runtime DRC flag is enabled:
++
++- inspect `delegate`
++- if it implements `io.unitycatalog.client.delta.DeltaRestClientProvider`, first check `getDeltaTablesApi()`
++- if `getDeltaTablesApi()` is empty, treat DRC as unavailable and fall back
++- otherwise obtain the shared UC `ApiClient`
++- build or reuse a `UCDeltaClient`
++- attempt DRC `loadTable`
++- attempt DRC `getTableCredentials(..., READ)`
++- convert the DRC schema directly to Spark schema
++- build the Delta table using the DRC metadata and READ credentials
++
++If any part of that DRC path fails, log:
++
++- `WARN: falling back to legacy UC API`
++
++and continue via the existing legacy `super.loadTable(...)` path.
++
++Fallback policy should stay simple:
++
++- if the runtime DRC flag is off, use the legacy path
++- if DRC is not available through `DeltaRestClientProvider`, use the legacy path
++- if the attempted DRC load path fails, log the WARN and use the legacy path
++
++The implementation should not try to encode a large taxonomy of fallback cases in the design or in control flow.
++
++## Metadata and Schema Adaptation
++
++### `DRCMetadataAdapter`
++
++Add a Delta-side adapter around the UC SDK `TableMetadata` implementing `AbstractMetadata`.
++
++Purpose:
++
++- expose Delta metadata through the storage abstraction
++- allow DRC-aware consumers to access the UC schema object directly
++- keep lazy `getSchemaString()` only as fallback / compatibility support
++
++This follows the useful shape of the POC while matching the current SDK, not the older SDK shape.
++
++### `DeltaRestSchemaConverter`
++
++Add a converter from the current UC SDK DRC schema model to Spark `StructType`.
++
++Requirements:
++
++- no JSON roundtrip on the happy path
++- support nested struct / array / map / decimal types
++- preserve field nullability
++- preserve field metadata needed by Delta and Spark, especially column mapping metadata
++
++The POC converter is the right idea but not authoritative implementation.
++
++## Behavior of Unsupported Methods in This Slice
++
++The following DRC methods may exist on `UCDeltaClient` now but should throw for this slice:
++
++- DRC `createTable`
++- DRC name-based `commit`
++- DRC name-based `getCommits` if added
++
++This keeps the interface shape aligned with the later design without accidentally implying write support exists.
++
++## File Impact
++
++Expected initial touch points:
++
++- `storage/.../uccommitcoordinator/UCDeltaClient.java`
++- `storage/.../uccommitcoordinator/` concrete implementation
++- `storage/.../uccommitcoordinator/DRCMetadataAdapter.java`
++- `spark/.../catalog/AbstractDeltaCatalog.scala`
++- `spark/.../catalog/DeltaRestSchemaConverter.scala`
++
++Explicitly not in this slice:
++
++- `UCCommitCoordinatorBuilder`
++- `CatalogTrackedInfo`
++- `SnapshotManagement`
++- `OptimisticTransaction`
++- kill switch weakening
++- DRC create / commit logic
++
++## Testing
++
++Required tests for this slice:
++
++- schema converter unit tests for primitive, decimal, array, map, and nested struct cases
++- field metadata preservation tests, especially column mapping metadata
++- successful DRC `loadTable` path
++- successful DRC `getTableCredentials(READ)` path
++- fallback when provider is absent
++- fallback when provider exposes no DRC support
++- fallback when DRC `loadTable` throws
++- fallback when DRC credential vending throws
++- WARN log assertion on fallback
++
++## Non-Goals
++
++This slice does not:
++
++- implement DRC `createTable`
++- implement DRC commit or update-table behavior
++- plumb etag through snapshot or commit state
++- share a Delta-owned `UCDeltaClient` instance through `UCDeltaClientProvider`
++- weaken UC-managed metadata kill switches
++
++Those belong to later slices.
\ No newline at end of file
storage/src/test/scala/io/delta/storage/commit/uccommitcoordinator/UCTokenBasedRestClientSuite.scala
@@ -62,7 +62,7 @@
      }
    }
  
-+  test("loadTable falls back to legacy UC API and converts table metadata") {
++  test("loadTable falls back to legacy UC API and converts metadata") {
 +    legacyTablesHandler = exchange => {
 +      assert(exchange.getRequestMethod === "GET")
 +      sendJson(
@@ -79,62 +79,36 @@
 +          |  "created_at": 10,
 +          |  "updated_at": 11,
 +          |  "properties": {"delta.appendOnly":"true"},
-+          |  "columns": [
-+          |    {"name":"value","type_text":"string","nullable":true,"position":1},
-+          |    {"name":"region","type_text":"string","nullable":false,"position":2,"partition_index":1},
-+          |    {"name":"date","type_text":"date","nullable":false,"position":3,"partition_index":0}
-+          |  ]
-+          |}""".stripMargin)
-+    }
-+
-+    withLegacyClient { client =>
-+      val response = client.loadTable("main", "default", "tbl")
-+      val metadata = response.getMetadata
-+      assert(metadata.getLocation === "s3://bucket/path/to/table")
-+      assert(metadata.getPartitionColumns === java.util.Arrays.asList("date", "region"))
-+      assert(metadata.getProperties.get("delta.appendOnly") === "true")
-+      assert(metadata.getColumns.getFields.size() === 3)
-+      assert(metadata.getColumns.getFields.get(1).getName === "region")
-+      assert(!metadata.getColumns.getFields.get(1).getNullable)
-+    }
-+  }
-+
-+  test("loadTable falls back to legacy UC API and parses type_json schemas") {
-+    legacyTablesHandler = exchange => {
-+      assert(exchange.getRequestMethod === "GET")
-+      sendJson(
-+        exchange,
-+        HttpStatus.SC_OK,
-+        """{
-+          |  "name": "tbl",
-+          |  "catalog_name": "main",
-+          |  "schema_name": "default",
-+          |  "table_id": "11111111-1111-1111-1111-111111111111",
-+          |  "table_type": "MANAGED",
-+          |  "data_source_format": "DELTA",
-+          |  "storage_location": "s3://bucket/path/to/table",
-+          |  "created_at": 10,
-+          |  "updated_at": 11,
 +          |  "columns": [
 +          |    {
 +          |      "name":"payload",
 +          |      "nullable":false,
 +          |      "position":0,
 +          |      "type_json":"{\"type\":\"struct\",\"fields\":[{\"name\":\"tags\",\"type\":{\"type\":\"array\",\"element-type\":\"string\",\"contains-null\":true},\"nullable\":true,\"metadata\":{\"comment\":\"nested tags\"}}]}"
-+          |    }
++          |    },
++          |    {"name":"value","type_text":"string","nullable":true,"position":1},
++          |    {"name":"region","type_text":"string","nullable":false,"position":2,"partition_index":1},
++          |    {"name":"date","type_text":"date","nullable":false,"position":3,"partition_index":0}
 +          |  ]
 +          |}""".stripMargin)
 +    }
 +
 +    withLegacyClient { client =>
 +      val response = client.loadTable("main", "default", "tbl")
-+      val payloadField = response.getMetadata.getColumns.getFields.get(0)
++      val metadata = response.getMetadata
++      val payloadField = metadata.getColumns.getFields.get(0)
 +      val payloadType = payloadField.getType.asInstanceOf[model.StructType]
 +      val nestedField = payloadType.getFields.get(0)
 +      val nestedType = nestedField.getType.asInstanceOf[model.ArrayType]
 +
++      assert(metadata.getLocation === "s3://bucket/path/to/table")
++      assert(metadata.getPartitionColumns === java.util.Arrays.asList("date", "region"))
++      assert(metadata.getProperties.get("delta.appendOnly") === "true")
++      assert(metadata.getColumns.getFields.size() === 4)
 +      assert(payloadField.getName === "payload")
 +      assert(!payloadField.getNullable)
++      assert(metadata.getColumns.getFields.get(2).getName === "region")
++      assert(!metadata.getColumns.getFields.get(2).getNullable)
 +      assert(nestedField.getName === "tags")
 +      assert(nestedField.getMetadata.get("comment") === "nested tags")
 +      assert(nestedType.getElementType.isInstanceOf[model.PrimitiveType])

Reproduce locally: git range-diff dc0340b..5c40a3a 5f1e465..8361784 | Disable: git config gitstack.push-range-diff false

@TimothyW553 TimothyW553 force-pushed the stack/drc-loadtable-storage branch 2 times, most recently from 4ae951d to 1ca5b16 Compare April 23, 2026 23:28
@TimothyW553
Copy link
Copy Markdown
Collaborator Author

Range-diff: master (4ae951d -> 1ca5b16)
docs/superpowers/specs/2026-04-22-drc-loadtable-design.md
@@ -0,0 +1,213 @@
+diff --git a/docs/superpowers/specs/2026-04-22-drc-loadtable-design.md b/docs/superpowers/specs/2026-04-22-drc-loadtable-design.md
+new file mode 100644
+--- /dev/null
++++ b/docs/superpowers/specs/2026-04-22-drc-loadtable-design.md
++# DRC LoadTable First Slice Design
++
++## Goal
++
++Implement the first Delta-side slice of Delta REST Catalog (DRC) integration for Unity Catalog:
++
++- DRC-backed `loadTable`
++- DRC-backed `getTableCredentials`
++
++This slice is intentionally read-only. It does not implement DRC `createTable`, DRC commit, or DRC get-commits behavior yet.
++
++## Authority and Scope
++
++This design is grounded in:
++
++- `delta_rest_catalog_design_doc_clarifications.md`
++- `delta_rest_catalog_api_spec.md`
++- `delta_rest_catalog_provider_design.md`
++- the current generated UC SDK under `unitycatalog/clients/java/target/src/main/java/io/unitycatalog/client/delta/`
++
++The old Delta POC PR [`delta-io/delta#6575`](https://github.com/delta-io/delta/pull/6575) is used only for shape:
++
++- `UCDeltaClient`
++- `DRCMetadataAdapter`
++- `DeltaRestSchemaConverter`
++- `AbstractDeltaCatalog.loadTable` wiring
++
++Any conflict between the POC and the current docs / SDK is resolved in favor of the docs and the current SDK.
++
++## Confirmed Decisions
++
++- Delta builds directly against UC master at a pinned SHA. No compile-time DRC shim architecture.
++- `UCDeltaClient` extends the existing `UCClient`.
++- The DRC read-side surface should mirror the server contract and return raw UC SDK types.
++- For this slice, the new read-side methods are name-based:
++  - `loadTable(catalog, schema, table)`
++  - `getTableCredentials(catalog, schema, table, operation)`
++- `tableId` is not an input to the read-side methods. It is returned by `LoadTableResponse.metadata.table-uuid`.
++- If the DRC path is unavailable or any DRC load/credential call fails, Delta must log a WARN and fall back to the legacy UC API path.
++- `UCDeltaClientProvider` is deferred. It belongs to the later shared-client / commit-coordinator slice, not this first load-only slice.
++
++## Contract Clarification: Credentials Are Separate
++
++The current local spec and generated SDK do not include credentials in `LoadTableResponse`.
++
++- The prose spec explicitly says credential vending is not part of `loadTable`.
++- The generated `LoadTableResponse` model contains only:
++  - `metadata`
++  - `commits`
++  - `uniform`
++  - `latest-table-version`
++
++Therefore, Delta must treat DRC table loading as two distinct RPCs:
++
++1. `TablesApi.loadTable(catalog, schema, table)`
++2. `TemporaryCredentialsApi.getTableCredentials(READ, catalog, schema, table)`
++
++## Recommended Architecture
++
++### 1. Delta-side client
++
++Add a Delta-side `UCDeltaClient` interface in storage that extends `UCClient`.
++
++For this first slice, it will expose:
++
++- legacy `UCClient` methods unchanged
++- new DRC read-side methods returning raw SDK types:
++  - `io.unitycatalog.client.delta.model.LoadTableResponse loadTable(...)`
++  - `io.unitycatalog.client.delta.model.CredentialsResponse getTableCredentials(...)`
++
++It will also define future DRC-shaped write methods now so the interface grows in the right direction:
++
++- DRC `createTable(...)`
++- DRC name-based `commit(...)`
++
++Those future methods should include the data the update path will need later:
++
++- `oldMetadata`
++- `newMetadata`
++- `oldProtocol`
++- `newProtocol`
++- optional `etag`
++- optional `uniform`
++
++For this slice, those future write methods throw immediately with a clear unsupported message.
++
++### 2. Concrete implementation
++
++Implement one concrete `UCDeltaClient` against the current UC client jars.
++
++It should:
++
++- reuse the UC-owned `ApiClient`
++- construct DRC `TablesApi` and `TemporaryCredentialsApi` from that `ApiClient`
++- use the existing legacy `UCClient` behavior as fallback
++
++This is not a stop-the-world refactor. The existing path-based legacy APIs remain intact.
++
++### 3. Catalog wiring
++
++`AbstractDeltaCatalog.loadTable` is the first DRC consumer.
++
++When the runtime DRC flag is enabled:
++
++- inspect `delegate`
++- if it implements `io.unitycatalog.client.delta.DeltaRestClientProvider`, first check `getDeltaTablesApi()`
++- if `getDeltaTablesApi()` is empty, treat DRC as unavailable and fall back
++- otherwise obtain the shared UC `ApiClient`
++- build or reuse a `UCDeltaClient`
++- attempt DRC `loadTable`
++- attempt DRC `getTableCredentials(..., READ)`
++- convert the DRC schema directly to Spark schema
++- build the Delta table using the DRC metadata and READ credentials
++
++If any part of that DRC path fails, log:
++
++- `WARN: falling back to legacy UC API`
++
++and continue via the existing legacy `super.loadTable(...)` path.
++
++Fallback policy should stay simple:
++
++- if the runtime DRC flag is off, use the legacy path
++- if DRC is not available through `DeltaRestClientProvider`, use the legacy path
++- if the attempted DRC load path fails, log the WARN and use the legacy path
++
++The implementation should not try to encode a large taxonomy of fallback cases in the design or in control flow.
++
++## Metadata and Schema Adaptation
++
++### `DRCMetadataAdapter`
++
++Add a Delta-side adapter around the UC SDK `TableMetadata` implementing `AbstractMetadata`.
++
++Purpose:
++
++- expose Delta metadata through the storage abstraction
++- allow DRC-aware consumers to access the UC schema object directly
++- keep lazy `getSchemaString()` only as fallback / compatibility support
++
++This follows the useful shape of the POC while matching the current SDK, not the older SDK shape.
++
++### `DeltaRestSchemaConverter`
++
++Add a converter from the current UC SDK DRC schema model to Spark `StructType`.
++
++Requirements:
++
++- no JSON roundtrip on the happy path
++- support nested struct / array / map / decimal types
++- preserve field nullability
++- preserve field metadata needed by Delta and Spark, especially column mapping metadata
++
++The POC converter is the right idea but not authoritative implementation.
++
++## Behavior of Unsupported Methods in This Slice
++
++The following DRC methods may exist on `UCDeltaClient` now but should throw for this slice:
++
++- DRC `createTable`
++- DRC name-based `commit`
++- DRC name-based `getCommits` if added
++
++This keeps the interface shape aligned with the later design without accidentally implying write support exists.
++
++## File Impact
++
++Expected initial touch points:
++
++- `storage/.../uccommitcoordinator/UCDeltaClient.java`
++- `storage/.../uccommitcoordinator/` concrete implementation
++- `storage/.../uccommitcoordinator/DRCMetadataAdapter.java`
++- `spark/.../catalog/AbstractDeltaCatalog.scala`
++- `spark/.../catalog/DeltaRestSchemaConverter.scala`
++
++Explicitly not in this slice:
++
++- `UCCommitCoordinatorBuilder`
++- `CatalogTrackedInfo`
++- `SnapshotManagement`
++- `OptimisticTransaction`
++- kill switch weakening
++- DRC create / commit logic
++
++## Testing
++
++Required tests for this slice:
++
++- schema converter unit tests for primitive, decimal, array, map, and nested struct cases
++- field metadata preservation tests, especially column mapping metadata
++- successful DRC `loadTable` path
++- successful DRC `getTableCredentials(READ)` path
++- fallback when provider is absent
++- fallback when provider exposes no DRC support
++- fallback when DRC `loadTable` throws
++- fallback when DRC credential vending throws
++- WARN log assertion on fallback
++
++## Non-Goals
++
++This slice does not:
++
++- implement DRC `createTable`
++- implement DRC commit or update-table behavior
++- plumb etag through snapshot or commit state
++- share a Delta-owned `UCDeltaClient` instance through `UCDeltaClientProvider`
++- weaken UC-managed metadata kill switches
++
++Those belong to later slices.
\ No newline at end of file

Reproduce locally: git range-diff 5f1e465..4ae951d 5f1e465..1ca5b16 | Disable: git config gitstack.push-range-diff false

@TimothyW553 TimothyW553 force-pushed the stack/drc-loadtable-storage branch 3 times, most recently from 99cc249 to 57e131c Compare April 24, 2026 05:09
@TimothyW553 TimothyW553 changed the title drc: add UC Delta REST client foundation [Storage][DRC] Add UC Delta REST client foundation Apr 24, 2026
@TimothyW553 TimothyW553 requested a review from yili-db April 24, 2026 06:10
@TimothyW553 TimothyW553 force-pushed the stack/drc-loadtable-storage branch 4 times, most recently from 0a7d129 to 6839aec Compare April 24, 2026 07:13
@TimothyW553 TimothyW553 force-pushed the stack/drc-loadtable-storage branch 2 times, most recently from 2473c52 to 1d0d53a Compare April 24, 2026 19:52
@TimothyW553 TimothyW553 requested a review from yili-db April 24, 2026 19:56

class UCDeltaRestCatalogUtilsSuite extends AnyFunSuite {

private val objectMapper = new ObjectMapper().registerModule(new DeltaTypeModule())
Copy link
Copy Markdown
Collaborator

@yili-db yili-db Apr 24, 2026

Choose a reason for hiding this comment

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

This diverges from production code where it is using ApiClient.getObjectMapper.
This code is trying to do the same as ApiClient.getObjectMapper but it may drift away.

Copy link
Copy Markdown
Collaborator Author

@TimothyW553 TimothyW553 Apr 24, 2026

Choose a reason for hiding this comment

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

The old test-local mapper is gone. This path parses legacy ColumnInfo.type_json (which uses camelCase), not a DRC response body, so it cannot use ApiClient.getObjectMapper(). The parsing now lives in UCLegacyLoadTableAdapter so its shared.

*/
public UCTokenBasedRestClient(
ApiClient apiClient,
Optional<io.unitycatalog.client.delta.api.TablesApi> deltaTablesApi) {
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.

Since we no longer need the shim, we can always create this UCTokenBasedRestClient with both tables API to make the construction interface simpler.
supportsDeltaRestCatalog can come from another way like a boolean.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. The existing-ApiClient constructor now takes a boolean instead of Optional<...TablesApi>.

Objects.requireNonNull(schema, "schema must not be null.");
Objects.requireNonNull(table, "table must not be null.");

boolean usingLegacyTablesApi = !supportsDeltaRestCatalog;
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.

this seems redundant.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. I removed that redundant boolean while cleaning up the loadTable branches.

catalog,
schema,
table);
}
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.

} else { for better readability

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done.

@TimothyW553 TimothyW553 changed the title [Storage] Add UC Delta API loadTable client APIs [Storage] Add UC Delta Rest Catalog API loadTable client APIs May 4, 2026
@TimothyW553 TimothyW553 force-pushed the stack/drc-loadtable-storage branch 2 times, most recently from d9fd9bf to 38a0fc6 Compare May 4, 2026 19:02
/**
* Interface for Unity Catalog Delta APIs.
*
* <p>This keeps UC Delta Rest Catalog API operations separate from legacy UC commit coordination.
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.

UC commit coordination -> UC client

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done, changed this to “legacy UC client” because this interface separates Delta API methods from the old UC client surface.

}
}

protected static class UCDeltaRestCatalogApiSupport {
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.

It's better to move this class before the first usage.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done, moved the helper class before the initialization methods that use it.

}

private static DataType toKernelType(io.unitycatalog.client.delta.model.DeltaType deltaType) {
if (deltaType instanceof io.unitycatalog.client.delta.model.PrimitiveType) {
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.

This file should only deal with Delta models, not the existing UC models. Then can you import these types?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

callers only see Delta storage models; the fully-qualified UC SDK types are kept only inside this conversion boundary because names like StructType and StructField conflict with Kernel types.

val adapter = metadata.asInstanceOf[UCDeltaTokenBasedRestClient.TableMetadataAdapter]
assert(adapter.getLocation === "file:/tmp/uc/table")
assert(adapter.getCreatedTime === Long.box(10L))
assert(adapter.getSchema.at(0).getName === "id")
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.

Should have more coverage on this function to test the schema conversion.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done, added schema conversion coverage for nested struct, array, map, decimal, metadata values, and nullability.

});

ApiClient apiClient = builder.build();
// Register the shared SDK client with the UC Delta base class without probing DRC support.
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.

DRC-> Delta API

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done, changed the wording from DRC to Delta API.

ApiClient apiClient = builder.build();
// Register the shared SDK client with the UC Delta base class without probing DRC support.
// The catalog-aware constructor below performs the optional config probe.
initializeUCDeltaRestCatalogApi(apiClient, false);
Copy link
Copy Markdown
Collaborator

@yili-db yili-db May 5, 2026

Choose a reason for hiding this comment

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

Which callers still call this constructor? Can they easily migrate to the one below?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

existing legacy commit-coordinator callers use this constructor and may not have a catalog, so this constructor stays non-probing while the catalog-aware constructor enables Delta API probing.

}
exchange.close()
})
server.createContext("/api/2.1/unity-catalog/delta/v1/config", exchange => {
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.

Can you move them into a separate test file?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done, moved the Delta API-specific client tests into UCDeltaTokenBasedRestClientSuite.


protected static boolean isUCDeltaRestCatalogApiEnabled() {
String deltaRestApiEnabled = System.getenv(UC_DELTA_REST_CATALOG_API_ENABLED);
return deltaRestApiEnabled == null || !deltaRestApiEnabled.equalsIgnoreCase("false");
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.

So by default, we will run those integration tests with the new api protocol ?

!deltaRestApiEnabled.equalsIgnoreCase("false");

will it be more easier to read if make it as deltaRestApiEnabled.equalsIgnoreCase("true") ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yes, default is enabled. i updated the code to make this explicit: unset means enabled, "true" means enabled, and anything else is disabled.

import java.util.List;

/** Delta-owned models for UC Delta table credentials. */
public final class UCDeltaModels {
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.

So those are the APIs that we will need to carefully maintain in future.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yes, these are Delta-owned boundary models for credential responses, and they stay small so callers do not depend on UC SDK credential model classes.

Comment on lines +80 to +84
protected final void initializeUCDeltaRestCatalogApi(
ApiClient apiClient,
String catalog) {
initializeUCDeltaRestCatalogApi(apiClient, getUCDeltaRestCatalogApiSupport(apiClient, catalog));
}
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.

Do we still need this ? I see no usage for this.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

removed the unused helper overload; the token client now probes support through configureUCDeltaRestCatalogApiSupport(catalog).


UC_DIR="${UC_DIR:-/tmp/unitycatalog}"
UC_REPO="${UC_REPO:-https://github.com/unitycatalog/unitycatalog.git}"
UC_REPO="${UC_REPO:-https://github.com/yili-db/unitycatalog.git}"
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.

this needs to be updated to some uc master branch reference + hash . .right?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yes, this is temporary

Copy link
Copy Markdown
Contributor

@tdas tdas left a comment

Choose a reason for hiding this comment

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

LGTM on build changes and high level direction.

JsonMapper.builder().serializationInclusion(JsonInclude.Include.NON_NULL).build();

static {
DELTA_TYPE_OBJECT_MAPPER.registerModule(new DeltaTypeModule());
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.

DeltaTypeModule is good for ser/deser the RPC shape columns which has kebab casing.
The schemaString is camelCasing. In UC server it used this to properly ser/deser the camelCasing:

  private static ObjectMapper createTypeMapper() {
    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(new DeltaTypeModule());
    mapper.addMixIn(ArrayType.class, CamelCaseArrayMixin.class);
    mapper.addMixIn(MapType.class, CamelCaseMapMixin.class);
    return mapper;
  }

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

fixed. added camelCase ArrayType/MapType mixins for schemaString serialization and a test covering kebab-case UC Delta API schema -> camelCase Delta schemaString.


private boolean supportsUCDeltaRestCatalogApi;
private volatile boolean closed;
private ApiClient deltaApiClient;
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.

nit: this ApiClient can be the same ApiClient used by UCTokenBasedRestClient. They can share the same ApiClient. It doesn't have to be done in this PR. Can be a follow up.

Copy link
Copy Markdown
Collaborator Author

@TimothyW553 TimothyW553 May 12, 2026

Choose a reason for hiding this comment

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

done . UCTokenBasedRestClient now passes its ApiClient to the UC Delta API base class, so both paths share the same ApiClient.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

we will do this in a follow up


@Override
public String getName() {
return null;
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.

null? Can you save the name in this class?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

fixed. TableMetadataAdapter now stores the table name passed from loadTable/createTable/updateTable and returns it from getName().

.set("spark.sql.catalog." + catalogName + ".token", uc.serverToken());
.set("spark.sql.catalog." + catalogName + ".token", uc.serverToken())
.set(
"spark.sql.catalog." + catalogName + ".deltaRestApi.enabled",
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.

qq: this catalog config actually won't affect the old and new credentials api in the unitycatalog's UCSingleCatalog , right ?

I think for next step we will need to add the PR in the unitycatalog repo to enable this catalog level spark config.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

right, this Delta-side config only controls whether DeltaCatalogClient tries the Delta API path. UC-side catalog-level credential config should be handled in the Unity Catalog follow-up.

Comment on lines +51 to +61
/**
* Gets temporary storage credentials for a table through the UC Delta Rest Catalog API.
*/
default CredentialsResponse getTableCredentials(
CredentialOperation operation,
String catalog,
String schema,
String table) throws IOException {
throw new UnsupportedOperationException(
"getTableCredentials requires UC Delta Rest Catalog API support.");
}
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.

@TimothyW553 @yili-db @tdas , do we really need this getTableCredentials API ? since the unitycatalog-hadoop public API will help us handle everything about the credentials, even for the credential initialziation and renewal itself.

My option is: we should entirely remove the getTableCredentials API definition here.

Copy link
Copy Markdown
Collaborator

@yili-db yili-db May 13, 2026

Choose a reason for hiding this comment

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

Right. Since unitycatalog/unitycatalog@ee60cae this is no longer needed here.
And the CredentialsResponse can be gone as well.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done, I removed getTableCredentials from the Delta storage client surface. table credential setup now goes through unitycatalog-hadoop instead.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done, CredentialsResponse is gone from this PR and the Delta API storage surface now only exposes loadTable.

Comment on lines +270 to +400
@Override
public CredentialsResponse getTableCredentials(
CredentialOperation operation,
String catalog,
String schema,
String table) throws IOException {
ensureUCDeltaRestCatalogApiSupported("getTableCredentials");
Objects.requireNonNull(operation, "operation must not be null.");
Objects.requireNonNull(catalog, "catalog must not be null.");
Objects.requireNonNull(schema, "schema must not be null.");
Objects.requireNonNull(table, "table must not be null.");

try {
return toCredentialsResponse(deltaTemporaryCredentialsApi.getTableCredentials(
toSdkCredentialOperation(operation),
catalog,
schema,
table));
} catch (ApiException e) {
throw new IOException(
String.format(
"Failed to get table credentials for %s.%s.%s (HTTP %s): %s",
catalog,
schema,
table,
e.getCode(),
e.getResponseBody()),
e);
}
}

private static TableMetadataAdapter toTableMetadata(String tableName, TableMetadata metadata) {
if (metadata == null) {
return null;
}
return new TableMetadataAdapter(tableName, metadata);
}

protected static CredentialsResponse toCredentialsResponse(
io.unitycatalog.client.delta.model.CredentialsResponse response) {
if (response == null) {
return null;
}
List<UCDeltaModels.StorageCredential> credentials = new ArrayList<>();
if (response.getStorageCredentials() != null) {
for (io.unitycatalog.client.delta.model.StorageCredential credential :
response.getStorageCredentials()) {
credentials.add(toStorageCredential(credential));
}
}
return new CredentialsResponse(credentials);
}

private static UCDeltaModels.StorageCredential toStorageCredential(
io.unitycatalog.client.delta.model.StorageCredential credential) {
if (credential == null) {
return null;
}
return new UCDeltaModels.StorageCredential(
credential.getPrefix(),
toCredentialOperation(credential.getOperation()),
toStorageCredentialConfig(credential.getConfig()),
credential.getExpirationTimeMs());
}

private static UCDeltaModels.StorageCredentialConfig toStorageCredentialConfig(
io.unitycatalog.client.delta.model.StorageCredentialConfig config) {
if (config == null) {
return null;
}
return new UCDeltaModels.StorageCredentialConfig(
getCredentialConfigValue(
config,
config.getS3AccessKeyId(),
io.unitycatalog.client.delta.model.StorageCredentialConfig
.JSON_PROPERTY_S3_ACCESS_KEY_ID),
getCredentialConfigValue(
config,
config.getS3SecretAccessKey(),
io.unitycatalog.client.delta.model.StorageCredentialConfig
.JSON_PROPERTY_S3_SECRET_ACCESS_KEY),
getCredentialConfigValue(
config,
config.getS3SessionToken(),
io.unitycatalog.client.delta.model.StorageCredentialConfig
.JSON_PROPERTY_S3_SESSION_TOKEN),
getCredentialConfigValue(
config,
config.getAzureSasToken(),
io.unitycatalog.client.delta.model.StorageCredentialConfig
.JSON_PROPERTY_AZURE_SAS_TOKEN),
getCredentialConfigValue(
config,
config.getGcsOauthToken(),
io.unitycatalog.client.delta.model.StorageCredentialConfig
.JSON_PROPERTY_GCS_OAUTH_TOKEN));
}

private static String getCredentialConfigValue(
io.unitycatalog.client.delta.model.StorageCredentialConfig config,
String getterValue,
String key) {
return getterValue != null ? getterValue : config.get(key);
}

private static CredentialOperation toCredentialOperation(
io.unitycatalog.client.delta.model.CredentialOperation operation) {
if (operation == null) {
return null;
}
switch (operation) {
case READ:
return CredentialOperation.READ;
case READ_WRITE:
return CredentialOperation.READ_WRITE;
default:
throw new IllegalArgumentException("Unsupported UC Delta credential operation: " + operation);
}
}

protected static io.unitycatalog.client.delta.model.CredentialOperation toSdkCredentialOperation(
CredentialOperation operation) {
switch (operation) {
case READ:
return io.unitycatalog.client.delta.model.CredentialOperation.READ;
case READ_WRITE:
return io.unitycatalog.client.delta.model.CredentialOperation.READ_WRITE;
default:
throw new IllegalArgumentException("Unsupported UC Delta credential operation: " + operation);
}
}
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.

I'm doubting if it's worth for us to duplicate the table credential code in the oss-delta repo again, considering we already have such a completed credentials support inside the oss-unitycatalog.

Anyway, I will take a serious look about the entire stacked PRs, and see if we could eliminate the code duplication. --- this is a serious problem i think.

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.

This entire block of code of getting table initial credential was written before unitycatalog/unitycatalog@ee60cae and was trying to mirror the same logic that was once in UCSingleCatalog.
It can be greatly simplified now.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

agreed, this was duplicating logic from unitycatalog-hadoop. I simplified this so Delta no longer owns table credential vending logic here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done, the table credential initialization moved to unitycatalog-hadoop in the Spark catalog layer, so UCDeltaTokenBasedRestClient only handles loadTable now.

String baseUri,
TokenProvider tokenProvider,
Map<String, String> appVersions,
String catalog) {
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 this catalog still using ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yes, the catalog arg is used by UCDeltaTokenBasedRestClient to probe catalog-scoped Delta API support; the shared provider constructor keeps the same signature so OSS and DBR can stay close.


ApiClientBuilder builder = ApiClientBuilder.create()
.uri(baseUri)
.tokenProvider(tokenProvider);
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.

unrelated: Do we also need to customized the retry policy ? not a blocker for this PR.

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.

Should be an easy fix: JitterDelayRetryPolicy.builder().build()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done, I added the default jitter retry policy through JitterDelayRetryPolicy.builder().build().

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done, added JitterDelayRetryPolicy.builder().build() in the shared ApiClient builder.

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