4141import java .util .Map ;
4242import java .util .Objects ;
4343import java .util .Optional ;
44+ import java .util .Set ;
4445import java .util .UUID ;
4546import java .util .concurrent .TimeUnit ;
47+ import java .util .concurrent .atomic .AtomicBoolean ;
48+ import java .util .concurrent .atomic .AtomicReference ;
49+ import java .util .function .BiFunction ;
4650import java .util .function .Consumer ;
51+ import java .util .function .Function ;
52+ import java .util .function .Supplier ;
4753import org .apache .hadoop .conf .Configuration ;
4854import org .apache .http .HttpHeaders ;
4955import org .apache .iceberg .BaseTable ;
7278import org .apache .iceberg .exceptions .ServiceFailureException ;
7379import org .apache .iceberg .expressions .Expressions ;
7480import org .apache .iceberg .inmemory .InMemoryCatalog ;
81+ import org .apache .iceberg .io .FileIO ;
7582import org .apache .iceberg .relocated .com .google .common .collect .ImmutableList ;
7683import org .apache .iceberg .relocated .com .google .common .collect .ImmutableMap ;
7784import org .apache .iceberg .relocated .com .google .common .collect .Lists ;
@@ -3107,6 +3114,153 @@ public void testCommitStateUnknownNotReconciled() {
31073114 .satisfies (ex -> assertThat (((CommitStateUnknownException ) ex ).getSuppressed ()).isEmpty ());
31083115 }
31093116
3117+ @ Test
3118+ public void testCustomTableOperationsInjection () throws IOException {
3119+ AtomicBoolean customTableOpsCalled = new AtomicBoolean ();
3120+ AtomicBoolean customTransactionTableOpsCalled = new AtomicBoolean ();
3121+ AtomicReference <RESTTableOperations > capturedOps = new AtomicReference <>();
3122+ RESTCatalogAdapter adapter = Mockito .spy (new RESTCatalogAdapter (backendCatalog ));
3123+ Map <String , String > customHeaders =
3124+ ImmutableMap .of ("X-Custom-Table-Header" , "custom-value-12345" );
3125+
3126+ // Custom RESTTableOperations that adds a custom header
3127+ class CustomRESTTableOperations extends RESTTableOperations {
3128+ CustomRESTTableOperations (
3129+ RESTClient client ,
3130+ String path ,
3131+ Supplier <Map <String , String >> headers ,
3132+ FileIO fileIO ,
3133+ TableMetadata current ,
3134+ Set <Endpoint > supportedEndpoints ) {
3135+ super (client , path , () -> customHeaders , fileIO , current , supportedEndpoints );
3136+ customTableOpsCalled .set (true );
3137+ }
3138+
3139+ CustomRESTTableOperations (
3140+ RESTClient client ,
3141+ String path ,
3142+ Supplier <Map <String , String >> headers ,
3143+ FileIO fileIO ,
3144+ RESTTableOperations .UpdateType updateType ,
3145+ List <MetadataUpdate > createChanges ,
3146+ TableMetadata current ,
3147+ Set <Endpoint > supportedEndpoints ) {
3148+ super (
3149+ client ,
3150+ path ,
3151+ () -> customHeaders ,
3152+ fileIO ,
3153+ updateType ,
3154+ createChanges ,
3155+ current ,
3156+ supportedEndpoints );
3157+ customTransactionTableOpsCalled .set (true );
3158+ }
3159+ }
3160+
3161+ // Custom RESTSessionCatalog that overrides table operations creation
3162+ class CustomRESTSessionCatalog extends RESTSessionCatalog {
3163+ CustomRESTSessionCatalog (
3164+ Function <Map <String , String >, RESTClient > clientBuilder ,
3165+ BiFunction <SessionCatalog .SessionContext , Map <String , String >, FileIO > ioBuilder ) {
3166+ super (clientBuilder , ioBuilder );
3167+ }
3168+
3169+ @ Override
3170+ protected RESTTableOperations newTableOps (
3171+ RESTClient restClient ,
3172+ String path ,
3173+ Supplier <Map <String , String >> headers ,
3174+ FileIO fileIO ,
3175+ TableMetadata current ,
3176+ Set <Endpoint > supportedEndpoints ) {
3177+ RESTTableOperations ops =
3178+ new CustomRESTTableOperations (
3179+ restClient , path , headers , fileIO , current , supportedEndpoints );
3180+ RESTTableOperations spy = Mockito .spy (ops );
3181+ capturedOps .set (spy );
3182+ return spy ;
3183+ }
3184+
3185+ @ Override
3186+ protected RESTTableOperations newTableOps (
3187+ RESTClient restClient ,
3188+ String path ,
3189+ Supplier <Map <String , String >> headers ,
3190+ FileIO fileIO ,
3191+ RESTTableOperations .UpdateType updateType ,
3192+ List <MetadataUpdate > createChanges ,
3193+ TableMetadata current ,
3194+ Set <Endpoint > supportedEndpoints ) {
3195+ RESTTableOperations ops =
3196+ new CustomRESTTableOperations (
3197+ restClient ,
3198+ path ,
3199+ headers ,
3200+ fileIO ,
3201+ updateType ,
3202+ createChanges ,
3203+ current ,
3204+ supportedEndpoints );
3205+ RESTTableOperations spy = Mockito .spy (ops );
3206+ capturedOps .set (spy );
3207+ return spy ;
3208+ }
3209+ }
3210+
3211+ try (RESTCatalog catalog =
3212+ catalog (adapter , clientBuilder -> new CustomRESTSessionCatalog (clientBuilder , null ))) {
3213+ catalog .createNamespace (NS );
3214+
3215+ // Test table operations without UpdateType
3216+ assertThat (customTableOpsCalled ).isFalse ();
3217+ assertThat (customTransactionTableOpsCalled ).isFalse ();
3218+ Table table = catalog .createTable (TABLE , SCHEMA );
3219+ assertThat (customTableOpsCalled ).isTrue ();
3220+ assertThat (customTransactionTableOpsCalled ).isFalse ();
3221+
3222+ // Trigger a commit through the custom operations
3223+ table .updateProperties ().set ("test-key" , "test-value" ).commit ();
3224+
3225+ // Verify the custom operations object was created and used
3226+ assertThat (capturedOps .get ()).isNotNull ();
3227+ Mockito .verify (capturedOps .get (), Mockito .atLeastOnce ()).current ();
3228+ Mockito .verify (capturedOps .get (), Mockito .atLeastOnce ()).commit (any (), any ());
3229+
3230+ // Verify the custom operations were used with custom headers
3231+ Mockito .verify (adapter , Mockito .atLeastOnce ())
3232+ .execute (
3233+ reqMatcher (HTTPMethod .POST , RESOURCE_PATHS .table (TABLE ), customHeaders ),
3234+ eq (LoadTableResponse .class ),
3235+ any (),
3236+ any ());
3237+
3238+ // Test table operations with UpdateType and createChanges
3239+ capturedOps .set (null );
3240+ customTableOpsCalled .set (false );
3241+ TableIdentifier table2 = TableIdentifier .of (NS , "table2" );
3242+ catalog .buildTable (table2 , SCHEMA ).createTransaction ().commitTransaction ();
3243+ assertThat (customTableOpsCalled ).isFalse ();
3244+ assertThat (customTransactionTableOpsCalled ).isTrue ();
3245+
3246+ // Trigger another commit to verify transaction operations also work
3247+ catalog .loadTable (table2 ).updateProperties ().set ("test-key-2" , "test-value-2" ).commit ();
3248+
3249+ // Verify the custom operations object was created and used
3250+ assertThat (capturedOps .get ()).isNotNull ();
3251+ Mockito .verify (capturedOps .get (), Mockito .atLeastOnce ()).current ();
3252+ Mockito .verify (capturedOps .get (), Mockito .atLeastOnce ()).commit (any (), any ());
3253+
3254+ // Verify the custom operations were used with custom headers
3255+ Mockito .verify (adapter , Mockito .atLeastOnce ())
3256+ .execute (
3257+ reqMatcher (HTTPMethod .POST , RESOURCE_PATHS .table (table2 ), customHeaders ),
3258+ eq (LoadTableResponse .class ),
3259+ any (),
3260+ any ());
3261+ }
3262+ }
3263+
31103264 private RESTCatalog catalog (RESTCatalogAdapter adapter ) {
31113265 RESTCatalog catalog =
31123266 new RESTCatalog (SessionCatalog .SessionContext .createEmpty (), (config ) -> adapter );
@@ -3117,6 +3271,25 @@ private RESTCatalog catalog(RESTCatalogAdapter adapter) {
31173271 return catalog ;
31183272 }
31193273
3274+ private RESTCatalog catalog (
3275+ RESTCatalogAdapter adapter ,
3276+ Function <Function <Map <String , String >, RESTClient >, RESTSessionCatalog >
3277+ sessionCatalogFactory ) {
3278+ RESTCatalog catalog =
3279+ new RESTCatalog (SessionCatalog .SessionContext .createEmpty (), (config ) -> adapter ) {
3280+ @ Override
3281+ protected RESTSessionCatalog newSessionCatalog (
3282+ Function <Map <String , String >, RESTClient > clientBuilder ) {
3283+ return sessionCatalogFactory .apply (clientBuilder );
3284+ }
3285+ };
3286+ catalog .initialize (
3287+ "test" ,
3288+ ImmutableMap .of (
3289+ CatalogProperties .FILE_IO_IMPL , "org.apache.iceberg.inmemory.InMemoryFileIO" ));
3290+ return catalog ;
3291+ }
3292+
31203293 static HTTPRequest reqMatcher (HTTPMethod method ) {
31213294 return argThat (req -> req .method () == method );
31223295 }
0 commit comments