diff --git a/paimon-api/src/main/java/org/apache/paimon/rest/RESTApi.java b/paimon-api/src/main/java/org/apache/paimon/rest/RESTApi.java index e8e1ff96c87b..12a368612c81 100644 --- a/paimon-api/src/main/java/org/apache/paimon/rest/RESTApi.java +++ b/paimon-api/src/main/java/org/apache/paimon/rest/RESTApi.java @@ -150,6 +150,15 @@ public class RESTApi { public static final String HEADER_PREFIX = "header."; + /** + * Optional header carrying the URL-encoded {@link Identifier} JSON of the table which initiated + * a dependency read. + * + *
This header only provides request context. Servers must not treat it as authorization + * proof. + */ + public static final String READ_VIA_HEADER = "X-Paimon-Read-Via"; + public static final String MAX_RESULTS = "maxResults"; public static final String PAGE_TOKEN = "pageToken"; diff --git a/paimon-core/src/main/java/org/apache/paimon/table/AppendOnlyFileStoreTable.java b/paimon-core/src/main/java/org/apache/paimon/table/AppendOnlyFileStoreTable.java index 4d35147c17a4..f5f59ee70c88 100644 --- a/paimon-core/src/main/java/org/apache/paimon/table/AppendOnlyFileStoreTable.java +++ b/paimon-core/src/main/java/org/apache/paimon/table/AppendOnlyFileStoreTable.java @@ -131,7 +131,7 @@ public InnerTableRead newRead() { ? new DataEvolutionTableRead( providerFactories, schema(), - catalogEnvironment.catalogContext(), + catalogEnvironment.dependencyReadContext(), () -> new AppendTableRead(providerFactories, schema())) : new AppendTableRead(providerFactories, schema()); } diff --git a/paimon-core/src/main/java/org/apache/paimon/table/BlobDescriptorReaderFactory.java b/paimon-core/src/main/java/org/apache/paimon/table/BlobDescriptorReaderFactory.java index a7c29ebfdbc8..bcbef2980451 100644 --- a/paimon-core/src/main/java/org/apache/paimon/table/BlobDescriptorReaderFactory.java +++ b/paimon-core/src/main/java/org/apache/paimon/table/BlobDescriptorReaderFactory.java @@ -20,6 +20,7 @@ import org.apache.paimon.catalog.Catalog; import org.apache.paimon.catalog.CatalogContext; +import org.apache.paimon.catalog.CatalogFactory; import org.apache.paimon.catalog.CatalogLoader; import org.apache.paimon.catalog.Identifier; import org.apache.paimon.fs.FileIO; @@ -49,14 +50,19 @@ public static UriReaderFactory create(FileStoreTable table) { } private static UriReaderFactory fromSourceTable(FileStoreTable table, String sourceTable) { + CatalogEnvironment catalogEnvironment = table.catalogEnvironment(); CatalogLoader catalogLoader = checkNotNull( - table.catalogEnvironment().catalogLoader(), + catalogEnvironment.catalogLoader(), "Option '%s' is not supported for tables without a catalog loader, " + "including external tables in REST catalogs.", BLOB_DESCRIPTOR_SOURCE_TABLE.key()); Identifier sourceIdentifier = Identifier.fromString(sourceTable); - try (Catalog catalog = catalogLoader.load()) { + CatalogContext dependencyContext = catalogEnvironment.dependencyReadContext(); + try (Catalog catalog = + dependencyContext == catalogEnvironment.catalogContext() + ? catalogLoader.load() + : CatalogFactory.createCatalog(dependencyContext)) { FileIO sourceFileIO = catalog.getTable(sourceIdentifier).fileIO(); // Initialize lazy credentials before serializing FileIO to distributed workers. sourceFileIO.isObjectStore(); diff --git a/paimon-core/src/main/java/org/apache/paimon/table/CatalogEnvironment.java b/paimon-core/src/main/java/org/apache/paimon/table/CatalogEnvironment.java index cb40c4447e18..5f75b7d0e58d 100644 --- a/paimon-core/src/main/java/org/apache/paimon/table/CatalogEnvironment.java +++ b/paimon-core/src/main/java/org/apache/paimon/table/CatalogEnvironment.java @@ -30,8 +30,14 @@ import org.apache.paimon.catalog.SnapshotCommit; import org.apache.paimon.catalog.TableRollback; import org.apache.paimon.operation.Lock; +import org.apache.paimon.options.Options; +import org.apache.paimon.rest.RESTApi; +import org.apache.paimon.rest.RESTCatalogFactory; +import org.apache.paimon.rest.RESTCatalogLoader; +import org.apache.paimon.rest.RESTUtil; import org.apache.paimon.table.source.TableQueryAuth; import org.apache.paimon.tag.SnapshotLoaderImpl; +import org.apache.paimon.utils.JsonSerdeUtil; import org.apache.paimon.utils.SnapshotLoader; import org.apache.paimon.utils.SnapshotManager; @@ -41,10 +47,13 @@ import java.util.Optional; import java.util.function.LongConsumer; +import static org.apache.paimon.options.CatalogOptions.METASTORE; + /** Catalog environment in table which contains log factory, metastore client factory. */ public class CatalogEnvironment implements Serializable { private static final long serialVersionUID = 2L; + private static final String READ_VIA_OPTION = RESTApi.HEADER_PREFIX + RESTApi.READ_VIA_HEADER; @Nullable private final Identifier identifier; @Nullable private final String uuid; @@ -196,6 +205,43 @@ public CatalogContext catalogContext() { return catalogContext; } + /** + * Returns a context for loading tables referenced while reading this table. + * + *
For REST catalogs, the outermost table identifier is attached as an optional request + * header. A context which already carries the header is returned unchanged so nested + * dependencies preserve the original table. + */ + @Nullable + CatalogContext dependencyReadContext() { + if (identifier == null || catalogContext == null) { + return catalogContext; + } + + boolean restCatalog = + catalogLoader instanceof RESTCatalogLoader + || RESTCatalogFactory.IDENTIFIER.equals( + catalogContext.options().get(METASTORE)); + if (!restCatalog) { + return catalogContext; + } + + Options options = catalogContext.options(); + if (options.containsKey(READ_VIA_OPTION)) { + return catalogContext; + } + + Options dependencyOptions = new Options(options.toMap()); + dependencyOptions.set(METASTORE, RESTCatalogFactory.IDENTIFIER); + dependencyOptions.set( + READ_VIA_OPTION, RESTUtil.encodeString(JsonSerdeUtil.toFlatJson(identifier))); + return CatalogContext.create( + dependencyOptions, + catalogContext.hadoopConf(), + catalogContext.preferIO(), + catalogContext.fallbackIO()); + } + public CatalogEnvironment copy(Identifier identifier) { return new CatalogEnvironment( identifier, diff --git a/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTCatalogTest.java b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTCatalogTest.java index ff3736d88903..2b91f1311b41 100644 --- a/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTCatalogTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTCatalogTest.java @@ -49,6 +49,8 @@ import org.apache.paimon.rest.exceptions.NotImplementedException; import org.apache.paimon.rest.responses.ConfigResponse; import org.apache.paimon.schema.Schema; +import org.apache.paimon.table.BlobDescriptorReaderFactory; +import org.apache.paimon.table.FileStoreTable; import org.apache.paimon.table.FormatTable; import org.apache.paimon.table.format.FormatTablePartitionManager; import org.apache.paimon.types.DataTypes; @@ -73,6 +75,7 @@ import static org.apache.paimon.catalog.Catalog.TABLE_DEFAULT_OPTION_PREFIX; import static org.apache.paimon.rest.RESTApi.HEADER_PREFIX; +import static org.apache.paimon.rest.RESTApi.READ_VIA_HEADER; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -420,6 +423,37 @@ void testBaseHeadersInRequests() throws Exception { checkHeader(customHeaderName, customHeaderValue); } + @Test + void testReadViaHeaderOnDependencyTableAndDataTokenRequests() throws Exception { + Identifier root = Identifier.create("db", "root"); + Identifier target = Identifier.create("db", "target"); + RESTCatalog restCatalog = initCatalog(true); + restCatalog.createDatabase(target.getDatabaseName(), true); + restCatalog.createTable(target, DEFAULT_TABLE_SCHEMA, false); + restCatalog.createTable( + root, + Schema.newBuilder() + .column("id", DataTypes.INT()) + .option( + CoreOptions.BLOB_DESCRIPTOR_SOURCE_TABLE.key(), + target.getFullName()) + .build(), + false); + FileStoreTable rootTable = (FileStoreTable) restCatalog.getTable(root); + + restCatalogServer.clearReceivedHeaders(); + BlobDescriptorReaderFactory.create(rootTable); + + String readVia = RESTUtil.encodeString(JsonSerdeUtil.toFlatJson(root)); + ResourcePaths resourcePaths = + ResourcePaths.forCatalogProperties(restCatalog.api().options()); + assertReadViaHeader( + resourcePaths.table(target.getDatabaseName(), target.getObjectName()), readVia); + assertReadViaHeader( + resourcePaths.tableToken(target.getDatabaseName(), target.getObjectName()), + readVia); + } + @Test void testCreateFormatTableWhenEnableDataToken() throws Exception { RESTCatalog restCatalog = initCatalog(true); @@ -495,6 +529,15 @@ private void checkHeader(String headerName, String headerValue) { assert foundCustomHeader : "Header was not found in any request"; } + private void assertReadViaHeader(String resourcePath, String readVia) { + assertThat(restCatalogServer.getReceivedHeaders(resourcePath)) + .singleElement() + .satisfies( + headers -> + assertThat(headers) + .containsEntry(READ_VIA_HEADER.toLowerCase(), readVia)); + } + private void testDlfAuth(RESTCatalog restCatalog) throws Exception { String databaseName = "db1"; restCatalog.createDatabase(databaseName, true); diff --git a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java index 6796e1ddf79e..b6ae35561f15 100644 --- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java +++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java @@ -226,6 +226,7 @@ public class RESTCatalogServer { private final ResourcePaths resourcePaths; private final List