|
62 | 62 | import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList; |
63 | 63 | import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap; |
64 | 64 | import org.apache.iceberg.relocated.com.google.common.collect.Lists; |
| 65 | +import org.apache.iceberg.rest.credentials.Credential; |
| 66 | +import org.apache.iceberg.rest.credentials.ImmutableCredential; |
65 | 67 | import org.apache.iceberg.rest.responses.ConfigResponse; |
66 | 68 | import org.apache.iceberg.rest.responses.ErrorResponse; |
| 69 | +import org.apache.iceberg.rest.responses.FetchPlanningResultResponse; |
| 70 | +import org.apache.iceberg.rest.responses.PlanTableScanResponse; |
67 | 71 | import org.junit.jupiter.api.Disabled; |
68 | 72 | import org.junit.jupiter.api.Test; |
69 | 73 | import org.junit.jupiter.params.ParameterizedTest; |
@@ -953,4 +957,110 @@ public void serverSupportsPlanningButNotCancellation() throws IOException { |
953 | 957 | // Verify no exception was thrown - cancelPlan returns false when endpoint not supported |
954 | 958 | assertThat(cancelled).isFalse(); |
955 | 959 | } |
| 960 | + |
| 961 | + @ParameterizedTest |
| 962 | + @EnumSource(PlanningMode.class) |
| 963 | + void fileIOForRemotePlanningIsPropagated( |
| 964 | + Function<TestPlanningBehavior.Builder, TestPlanningBehavior.Builder> planMode) { |
| 965 | + RESTCatalogAdapter adapter = |
| 966 | + Mockito.spy( |
| 967 | + new RESTCatalogAdapter(backendCatalog) { |
| 968 | + @Override |
| 969 | + public <T extends RESTResponse> T execute( |
| 970 | + HTTPRequest request, |
| 971 | + Class<T> responseType, |
| 972 | + Consumer<ErrorResponse> errorHandler, |
| 973 | + Consumer<Map<String, String>> responseHeaders, |
| 974 | + ParserContext parserContext) { |
| 975 | + T response = |
| 976 | + super.execute( |
| 977 | + request, responseType, errorHandler, responseHeaders, parserContext); |
| 978 | + return maybeAddStorageCredential(response); |
| 979 | + } |
| 980 | + }); |
| 981 | + |
| 982 | + adapter.setPlanningBehavior(planMode.apply(TestPlanningBehavior.builder()).build()); |
| 983 | + |
| 984 | + RESTCatalog catalog = |
| 985 | + new RESTCatalog(SessionCatalog.SessionContext.createEmpty(), (config) -> adapter); |
| 986 | + catalog.initialize( |
| 987 | + "test", |
| 988 | + ImmutableMap.of( |
| 989 | + CatalogProperties.FILE_IO_IMPL, |
| 990 | + "org.apache.iceberg.inmemory.InMemoryFileIO", |
| 991 | + RESTCatalogProperties.REST_SCAN_PLANNING_ENABLED, |
| 992 | + "true")); |
| 993 | + |
| 994 | + Table table = restTableFor(catalog, "file_io_propagation"); |
| 995 | + setParserContext(table); |
| 996 | + |
| 997 | + assertThat(table.io().properties()).doesNotContainKey(RESTCatalogProperties.REST_SCAN_PLAN_ID); |
| 998 | + |
| 999 | + TableScan tableScan = table.newScan(); |
| 1000 | + assertThat(tableScan.io().properties()) |
| 1001 | + .isSameAs(table.io().properties()) |
| 1002 | + .doesNotContainKey(RESTCatalogProperties.REST_SCAN_PLAN_ID); |
| 1003 | + // make sure remote scan planning is called and FileIO gets the planId |
| 1004 | + assertThat(tableScan.planFiles()).hasSize(1); |
| 1005 | + assertThat(table.io().properties()).doesNotContainKey(RESTCatalogProperties.REST_SCAN_PLAN_ID); |
| 1006 | + assertThat(tableScan.io().properties()).containsKey(RESTCatalogProperties.REST_SCAN_PLAN_ID); |
| 1007 | + String planId = tableScan.io().properties().get(RESTCatalogProperties.REST_SCAN_PLAN_ID); |
| 1008 | + |
| 1009 | + TableScan newScan = table.newScan(); |
| 1010 | + assertThat(newScan.io().properties()) |
| 1011 | + .isSameAs(table.io().properties()) |
| 1012 | + .doesNotContainKey(RESTCatalogProperties.REST_SCAN_PLAN_ID); |
| 1013 | + // make sure remote scan planning is called and FileIO gets the planId |
| 1014 | + assertThat(newScan.planFiles()).hasSize(1); |
| 1015 | + assertThat(table.io().properties()).doesNotContainKey(RESTCatalogProperties.REST_SCAN_PLAN_ID); |
| 1016 | + |
| 1017 | + // make sure planIds are different for each scan |
| 1018 | + assertThat(newScan.io().properties()).containsKey(RESTCatalogProperties.REST_SCAN_PLAN_ID); |
| 1019 | + assertThat(newScan.io().properties().get(RESTCatalogProperties.REST_SCAN_PLAN_ID)) |
| 1020 | + .isNotEqualTo(planId); |
| 1021 | + } |
| 1022 | + |
| 1023 | + @SuppressWarnings("unchecked") |
| 1024 | + private <T extends RESTResponse> T maybeAddStorageCredential(T response) { |
| 1025 | + if (response instanceof PlanTableScanResponse resp |
| 1026 | + && PlanStatus.COMPLETED == resp.planStatus()) { |
| 1027 | + return (T) |
| 1028 | + PlanTableScanResponse.builder() |
| 1029 | + .withPlanStatus(resp.planStatus()) |
| 1030 | + .withPlanId(resp.planId()) |
| 1031 | + .withPlanTasks(resp.planTasks()) |
| 1032 | + .withFileScanTasks(resp.fileScanTasks()) |
| 1033 | + .withCredentials( |
| 1034 | + ImmutableList.<Credential>builder() |
| 1035 | + .addAll(resp.credentials()) |
| 1036 | + .add( |
| 1037 | + ImmutableCredential.builder() |
| 1038 | + .prefix("dummy") |
| 1039 | + .putConfig("dummyKey", "dummyVal") |
| 1040 | + .build()) |
| 1041 | + .build()) |
| 1042 | + .withSpecsById(resp.specsById()) |
| 1043 | + .build(); |
| 1044 | + } else if (response instanceof FetchPlanningResultResponse resp |
| 1045 | + && PlanStatus.COMPLETED == resp.planStatus()) { |
| 1046 | + return (T) |
| 1047 | + FetchPlanningResultResponse.builder() |
| 1048 | + .withPlanStatus(resp.planStatus()) |
| 1049 | + .withFileScanTasks(resp.fileScanTasks()) |
| 1050 | + .withPlanTasks(resp.planTasks()) |
| 1051 | + .withSpecsById(resp.specsById()) |
| 1052 | + .withCredentials( |
| 1053 | + ImmutableList.<Credential>builder() |
| 1054 | + .addAll(resp.credentials()) |
| 1055 | + .add( |
| 1056 | + ImmutableCredential.builder() |
| 1057 | + .prefix("dummy") |
| 1058 | + .putConfig("dummyKey", "dummyVal") |
| 1059 | + .build()) |
| 1060 | + .build()) |
| 1061 | + .build(); |
| 1062 | + } |
| 1063 | + |
| 1064 | + return response; |
| 1065 | + } |
956 | 1066 | } |
0 commit comments