This is an automated email from the ASF dual-hosted git repository.

blue pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iceberg.git


The following commit(s) were added to refs/heads/master by this push:
     new f3826bd9b1 Core: Support registerTable with REST session catalog 
(#6512)
f3826bd9b1 is described below

commit f3826bd9b1111d08262109df741ca8bf5aa0b697
Author: Vikash Kumar <[email protected]>
AuthorDate: Wed Jul 5 02:36:09 2023 +0530

    Core: Support registerTable with REST session catalog (#6512)
---
 .../org/apache/iceberg/rest/CatalogHandlers.java   | 16 ++++
 .../org/apache/iceberg/rest/RESTSerializers.java   | 28 ++++++-
 .../apache/iceberg/rest/RESTSessionCatalog.java    | 37 +++++++++-
 .../org/apache/iceberg/rest/ResourcePaths.java     |  4 +
 .../rest/requests/RegisterTableRequest.java        | 35 +++++++++
 .../rest/requests/RegisterTableRequestParser.java  | 69 +++++++++++++++++
 .../org/apache/iceberg/catalog/CatalogTests.java   | 86 ++++++++++++++++++++++
 .../org/apache/iceberg/jdbc/TestJdbcCatalog.java   | 33 ---------
 .../apache/iceberg/rest/RESTCatalogAdapter.java    | 14 ++++
 .../org/apache/iceberg/rest/TestResourcePaths.java |  8 ++
 .../requests/TestRegisterTableRequestParser.java   | 80 ++++++++++++++++++++
 open-api/rest-catalog-open-api.py                  |  5 ++
 open-api/rest-catalog-open-api.yaml                | 63 ++++++++++++++++
 13 files changed, 443 insertions(+), 35 deletions(-)

diff --git a/core/src/main/java/org/apache/iceberg/rest/CatalogHandlers.java 
b/core/src/main/java/org/apache/iceberg/rest/CatalogHandlers.java
index 6fddb08757..1e0ef66027 100644
--- a/core/src/main/java/org/apache/iceberg/rest/CatalogHandlers.java
+++ b/core/src/main/java/org/apache/iceberg/rest/CatalogHandlers.java
@@ -54,6 +54,7 @@ import 
org.apache.iceberg.relocated.com.google.common.collect.Maps;
 import org.apache.iceberg.relocated.com.google.common.collect.Sets;
 import org.apache.iceberg.rest.requests.CreateNamespaceRequest;
 import org.apache.iceberg.rest.requests.CreateTableRequest;
+import org.apache.iceberg.rest.requests.RegisterTableRequest;
 import org.apache.iceberg.rest.requests.RenameTableRequest;
 import org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest;
 import org.apache.iceberg.rest.requests.UpdateTableRequest;
@@ -223,6 +224,21 @@ public class CatalogHandlers {
     throw new IllegalStateException("Cannot wrap catalog that does not produce 
BaseTable");
   }
 
+  public static LoadTableResponse registerTable(
+      Catalog catalog, Namespace namespace, RegisterTableRequest request) {
+    request.validate();
+
+    TableIdentifier identifier = TableIdentifier.of(namespace, request.name());
+    Table table = catalog.registerTable(identifier, 
request.metadataLocation());
+    if (table instanceof BaseTable) {
+      return LoadTableResponse.builder()
+          .withTableMetadata(((BaseTable) table).operations().current())
+          .build();
+    }
+
+    throw new IllegalStateException("Cannot wrap catalog that does not produce 
BaseTable");
+  }
+
   public static void dropTable(Catalog catalog, TableIdentifier ident) {
     boolean dropped = catalog.dropTable(ident, false);
     if (!dropped) {
diff --git a/core/src/main/java/org/apache/iceberg/rest/RESTSerializers.java 
b/core/src/main/java/org/apache/iceberg/rest/RESTSerializers.java
index 2bf95fea03..06f2de32df 100644
--- a/core/src/main/java/org/apache/iceberg/rest/RESTSerializers.java
+++ b/core/src/main/java/org/apache/iceberg/rest/RESTSerializers.java
@@ -44,7 +44,10 @@ import org.apache.iceberg.catalog.TableIdentifierParser;
 import org.apache.iceberg.rest.auth.OAuth2Util;
 import org.apache.iceberg.rest.requests.CommitTransactionRequest;
 import org.apache.iceberg.rest.requests.CommitTransactionRequestParser;
+import org.apache.iceberg.rest.requests.ImmutableRegisterTableRequest;
 import org.apache.iceberg.rest.requests.ImmutableReportMetricsRequest;
+import org.apache.iceberg.rest.requests.RegisterTableRequest;
+import org.apache.iceberg.rest.requests.RegisterTableRequestParser;
 import org.apache.iceberg.rest.requests.ReportMetricsRequest;
 import org.apache.iceberg.rest.requests.ReportMetricsRequestParser;
 import org.apache.iceberg.rest.requests.UpdateRequirementParser;
@@ -93,7 +96,12 @@ public class RESTSerializers {
         .addSerializer(CommitTransactionRequest.class, new 
CommitTransactionRequestSerializer())
         .addDeserializer(CommitTransactionRequest.class, new 
CommitTransactionRequestDeserializer())
         .addSerializer(UpdateTableRequest.class, new 
UpdateTableRequestSerializer())
-        .addDeserializer(UpdateTableRequest.class, new 
UpdateTableRequestDeserializer());
+        .addDeserializer(UpdateTableRequest.class, new 
UpdateTableRequestDeserializer())
+        .addSerializer(RegisterTableRequest.class, new 
RegisterTableRequestSerializer<>())
+        .addDeserializer(RegisterTableRequest.class, new 
RegisterTableRequestDeserializer<>())
+        .addSerializer(ImmutableRegisterTableRequest.class, new 
RegisterTableRequestSerializer<>())
+        .addDeserializer(
+            ImmutableRegisterTableRequest.class, new 
RegisterTableRequestDeserializer<>());
     mapper.registerModule(module);
   }
 
@@ -353,4 +361,22 @@ public class RESTSerializers {
       return UpdateTableRequestParser.fromJson(jsonNode);
     }
   }
+
+  public static class RegisterTableRequestSerializer<T extends 
RegisterTableRequest>
+      extends JsonSerializer<T> {
+    @Override
+    public void serialize(T request, JsonGenerator gen, SerializerProvider 
serializers)
+        throws IOException {
+      RegisterTableRequestParser.toJson(request, gen);
+    }
+  }
+
+  public static class RegisterTableRequestDeserializer<T extends 
RegisterTableRequest>
+      extends JsonDeserializer<T> {
+    @Override
+    public T deserialize(JsonParser p, DeserializationContext context) throws 
IOException {
+      JsonNode jsonNode = p.getCodec().readTree(p);
+      return (T) RegisterTableRequestParser.fromJson(jsonNode);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java 
b/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
index 884e35dd62..72547eec34 100644
--- a/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
+++ b/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
@@ -71,6 +71,8 @@ import org.apache.iceberg.rest.auth.OAuth2Util.AuthSession;
 import org.apache.iceberg.rest.requests.CommitTransactionRequest;
 import org.apache.iceberg.rest.requests.CreateNamespaceRequest;
 import org.apache.iceberg.rest.requests.CreateTableRequest;
+import org.apache.iceberg.rest.requests.ImmutableRegisterTableRequest;
+import org.apache.iceberg.rest.requests.RegisterTableRequest;
 import org.apache.iceberg.rest.requests.RenameTableRequest;
 import org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest;
 import org.apache.iceberg.rest.requests.UpdateTableRequest;
@@ -404,7 +406,40 @@ public class RESTSessionCatalog extends BaseSessionCatalog
   @Override
   public Table registerTable(
       SessionContext context, TableIdentifier ident, String 
metadataFileLocation) {
-    throw new UnsupportedOperationException("Register table is not supported");
+    checkIdentifierIsValid(ident);
+
+    Preconditions.checkArgument(
+        metadataFileLocation != null && !metadataFileLocation.isEmpty(),
+        "Invalid metadata file location: %s",
+        metadataFileLocation);
+
+    RegisterTableRequest request =
+        ImmutableRegisterTableRequest.builder()
+            .name(ident.name())
+            .metadataLocation(metadataFileLocation)
+            .build();
+
+    LoadTableResponse response =
+        client.post(
+            paths.register(ident.namespace()),
+            request,
+            LoadTableResponse.class,
+            headers(context),
+            ErrorHandlers.tableErrorHandler());
+
+    AuthSession session = tableSession(response.config(), session(context));
+    RESTTableOperations ops =
+        new RESTTableOperations(
+            client,
+            paths.table(ident),
+            session::headers,
+            tableFileIO(context, response.config()),
+            response.tableMetadata());
+
+    trackFileIO(ops);
+
+    return new BaseTable(
+        ops, fullTableName(ident), metricsReporter(paths.metrics(ident), 
session::headers));
   }
 
   @Override
diff --git a/core/src/main/java/org/apache/iceberg/rest/ResourcePaths.java 
b/core/src/main/java/org/apache/iceberg/rest/ResourcePaths.java
index 7e4f9e0c98..b5b974f14c 100644
--- a/core/src/main/java/org/apache/iceberg/rest/ResourcePaths.java
+++ b/core/src/main/java/org/apache/iceberg/rest/ResourcePaths.java
@@ -71,6 +71,10 @@ public class ResourcePaths {
         RESTUtil.encodeString(ident.name()));
   }
 
+  public String register(Namespace ns) {
+    return SLASH.join("v1", prefix, "namespaces", 
RESTUtil.encodeNamespace(ns), "register");
+  }
+
   public String rename() {
     return SLASH.join("v1", prefix, "tables", "rename");
   }
diff --git 
a/core/src/main/java/org/apache/iceberg/rest/requests/RegisterTableRequest.java 
b/core/src/main/java/org/apache/iceberg/rest/requests/RegisterTableRequest.java
new file mode 100644
index 0000000000..33b37dae24
--- /dev/null
+++ 
b/core/src/main/java/org/apache/iceberg/rest/requests/RegisterTableRequest.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.rest.requests;
+
+import org.apache.iceberg.rest.RESTRequest;
+import org.immutables.value.Value;
+
[email protected]
+public interface RegisterTableRequest extends RESTRequest {
+
+  String name();
+
+  String metadataLocation();
+
+  @Override
+  default void validate() {
+    // nothing to validate as it's not possible to create an invalid instance
+  }
+}
diff --git 
a/core/src/main/java/org/apache/iceberg/rest/requests/RegisterTableRequestParser.java
 
b/core/src/main/java/org/apache/iceberg/rest/requests/RegisterTableRequestParser.java
new file mode 100644
index 0000000000..961b6c185b
--- /dev/null
+++ 
b/core/src/main/java/org/apache/iceberg/rest/requests/RegisterTableRequestParser.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.rest.requests;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonNode;
+import java.io.IOException;
+import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
+import org.apache.iceberg.util.JsonUtil;
+
+public class RegisterTableRequestParser {
+
+  private static final String NAME = "name";
+  private static final String METADATA_LOCATION = "metadata-location";
+
+  private RegisterTableRequestParser() {}
+
+  public static String toJson(RegisterTableRequest request) {
+    return toJson(request, false);
+  }
+
+  public static String toJson(RegisterTableRequest request, boolean pretty) {
+    return JsonUtil.generate(gen -> toJson(request, gen), pretty);
+  }
+
+  public static void toJson(RegisterTableRequest request, JsonGenerator gen) 
throws IOException {
+    Preconditions.checkArgument(null != request, "Invalid register table 
request: null");
+
+    gen.writeStartObject();
+
+    gen.writeStringField(NAME, request.name());
+    gen.writeStringField(METADATA_LOCATION, request.metadataLocation());
+
+    gen.writeEndObject();
+  }
+
+  public static RegisterTableRequest fromJson(String json) {
+    return JsonUtil.parse(json, RegisterTableRequestParser::fromJson);
+  }
+
+  public static RegisterTableRequest fromJson(JsonNode json) {
+    Preconditions.checkArgument(
+        null != json, "Cannot parse register table request from null object");
+
+    String name = JsonUtil.getString(NAME, json);
+    String metadataLocation = JsonUtil.getString(METADATA_LOCATION, json);
+
+    return ImmutableRegisterTableRequest.builder()
+        .name(name)
+        .metadataLocation(metadataLocation)
+        .build();
+  }
+}
diff --git a/core/src/test/java/org/apache/iceberg/catalog/CatalogTests.java 
b/core/src/test/java/org/apache/iceberg/catalog/CatalogTests.java
index 15259d979e..5641dce89f 100644
--- a/core/src/test/java/org/apache/iceberg/catalog/CatalogTests.java
+++ b/core/src/test/java/org/apache/iceberg/catalog/CatalogTests.java
@@ -42,6 +42,7 @@ import org.apache.iceberg.SortOrder;
 import org.apache.iceberg.Table;
 import org.apache.iceberg.TableOperations;
 import org.apache.iceberg.TableProperties;
+import org.apache.iceberg.TestHelpers;
 import org.apache.iceberg.Transaction;
 import org.apache.iceberg.UpdatePartitionSpec;
 import org.apache.iceberg.UpdateSchema;
@@ -2615,6 +2616,91 @@ public abstract class CatalogTests<C extends Catalog & 
SupportsNamespaces> {
         .hasMessageContaining("Namespace does not exist: non-existing");
   }
 
+  @Test
+  public void testRegisterTable() {
+    C catalog = catalog();
+
+    if (requiresNamespaceCreate()) {
+      catalog.createNamespace(TABLE.namespace());
+    }
+
+    Map<String, String> properties =
+        ImmutableMap.of("user", "someone", "created-at", 
"2023-01-15T00:00:01");
+    Table originalTable =
+        catalog
+            .buildTable(TABLE, SCHEMA)
+            .withPartitionSpec(SPEC)
+            .withSortOrder(WRITE_ORDER)
+            .withProperties(properties)
+            .create();
+
+    originalTable.newFastAppend().appendFile(FILE_A).commit();
+    originalTable.newFastAppend().appendFile(FILE_B).commit();
+    originalTable.newDelete().deleteFile(FILE_A).commit();
+    originalTable.newFastAppend().appendFile(FILE_C).commit();
+
+    TableOperations ops = ((BaseTable) originalTable).operations();
+    String metadataLocation = ops.current().metadataFileLocation();
+
+    catalog.dropTable(TABLE, false /* do not purge */);
+
+    Table registeredTable = catalog.registerTable(TABLE, metadataLocation);
+
+    Assertions.assertThat(registeredTable).isNotNull();
+    Assertions.assertThat(catalog.tableExists(TABLE)).as("Table must 
exist").isTrue();
+    Assertions.assertThat(registeredTable.properties())
+        .as("Props must match")
+        .containsAllEntriesOf(properties);
+    Assertions.assertThat(registeredTable.schema().asStruct())
+        .as("Schema must match")
+        .isEqualTo(originalTable.schema().asStruct());
+    Assertions.assertThat(registeredTable.specs())
+        .as("Specs must match")
+        .isEqualTo(originalTable.specs());
+    Assertions.assertThat(registeredTable.sortOrders())
+        .as("Sort orders must match")
+        .isEqualTo(originalTable.sortOrders());
+    Assertions.assertThat(registeredTable.currentSnapshot())
+        .as("Current snapshot must match")
+        .isEqualTo(originalTable.currentSnapshot());
+    Assertions.assertThat(registeredTable.snapshots())
+        .as("Snapshots must match")
+        .isEqualTo(originalTable.snapshots());
+    Assertions.assertThat(registeredTable.history())
+        .as("History must match")
+        .isEqualTo(originalTable.history());
+
+    TestHelpers.assertSameSchemaMap(registeredTable.schemas(), 
originalTable.schemas());
+    assertFiles(registeredTable, FILE_B, FILE_C);
+
+    registeredTable.newFastAppend().appendFile(FILE_A).commit();
+    assertFiles(registeredTable, FILE_B, FILE_C, FILE_A);
+
+    Assertions.assertThat(catalog.loadTable(TABLE)).isNotNull();
+    Assertions.assertThat(catalog.dropTable(TABLE)).isTrue();
+    Assertions.assertThat(catalog.tableExists(TABLE)).isFalse();
+  }
+
+  @Test
+  public void testRegisterExistingTable() {
+    C catalog = catalog();
+
+    TableIdentifier identifier = TableIdentifier.of("a", "t1");
+
+    if (requiresNamespaceCreate()) {
+      catalog.createNamespace(identifier.namespace());
+    }
+
+    catalog.createTable(identifier, SCHEMA);
+    Table table = catalog.loadTable(identifier);
+    TableOperations ops = ((BaseTable) table).operations();
+    String metadataLocation = ops.current().metadataFileLocation();
+    Assertions.assertThatThrownBy(() -> catalog.registerTable(identifier, 
metadataLocation))
+        .isInstanceOf(AlreadyExistsException.class)
+        .hasMessage("Table already exists: a.t1");
+    Assertions.assertThat(catalog.dropTable(identifier)).isTrue();
+  }
+
   private static void assertEmpty(String context, Catalog catalog, Namespace 
ns) {
     try {
       Assertions.assertThat(catalog.listTables(ns)).as(context).isEmpty();
diff --git a/core/src/test/java/org/apache/iceberg/jdbc/TestJdbcCatalog.java 
b/core/src/test/java/org/apache/iceberg/jdbc/TestJdbcCatalog.java
index 73520cb941..4634de5707 100644
--- a/core/src/test/java/org/apache/iceberg/jdbc/TestJdbcCatalog.java
+++ b/core/src/test/java/org/apache/iceberg/jdbc/TestJdbcCatalog.java
@@ -44,13 +44,11 @@ import org.apache.iceberg.DataFile;
 import org.apache.iceberg.DataFiles;
 import org.apache.iceberg.FileFormat;
 import org.apache.iceberg.FileScanTask;
-import org.apache.iceberg.HasTableOperations;
 import org.apache.iceberg.PartitionSpec;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.SortOrder;
 import org.apache.iceberg.Table;
 import org.apache.iceberg.TableOperations;
-import org.apache.iceberg.TestHelpers;
 import org.apache.iceberg.Transaction;
 import org.apache.iceberg.catalog.CatalogTests;
 import org.apache.iceberg.catalog.Namespace;
@@ -751,37 +749,6 @@ public class TestJdbcCatalog extends 
CatalogTests<JdbcCatalog> {
     assertThat(JdbcUtil.stringToNamespace(nsString)).isEqualTo(ns);
   }
 
-  @Test
-  public void testRegisterTable() {
-    TableIdentifier identifier = TableIdentifier.of("a", "t1");
-    catalog.createTable(identifier, SCHEMA);
-    Table registeringTable = catalog.loadTable(identifier);
-    catalog.dropTable(identifier, false);
-    TableOperations ops = ((HasTableOperations) registeringTable).operations();
-    String metadataLocation = ((JdbcTableOperations) 
ops).currentMetadataLocation();
-    Table registeredTable = catalog.registerTable(identifier, 
metadataLocation);
-    Assertions.assertThat(registeredTable).isNotNull();
-    TestHelpers.assertSerializedAndLoadedMetadata(registeringTable, 
registeredTable);
-    String expectedMetadataLocation =
-        ((HasTableOperations) 
registeredTable).operations().current().metadataFileLocation();
-    
Assertions.assertThat(metadataLocation).isEqualTo(expectedMetadataLocation);
-    Assertions.assertThat(catalog.loadTable(identifier)).isNotNull();
-    Assertions.assertThat(catalog.dropTable(identifier)).isTrue();
-  }
-
-  @Test
-  public void testRegisterExistingTable() {
-    TableIdentifier identifier = TableIdentifier.of("a", "t1");
-    catalog.createTable(identifier, SCHEMA);
-    Table registeringTable = catalog.loadTable(identifier);
-    TableOperations ops = ((HasTableOperations) registeringTable).operations();
-    String metadataLocation = ((JdbcTableOperations) 
ops).currentMetadataLocation();
-    Assertions.assertThatThrownBy(() -> catalog.registerTable(identifier, 
metadataLocation))
-        .isInstanceOf(AlreadyExistsException.class)
-        .hasMessage("Table already exists: a.t1");
-    Assertions.assertThat(catalog.dropTable(identifier)).isTrue();
-  }
-
   @Test
   public void testCatalogWithCustomMetricsReporter() throws IOException {
     JdbcCatalog catalogWithCustomReporter =
diff --git a/core/src/test/java/org/apache/iceberg/rest/RESTCatalogAdapter.java 
b/core/src/test/java/org/apache/iceberg/rest/RESTCatalogAdapter.java
index 2990fcca78..0772601b77 100644
--- a/core/src/test/java/org/apache/iceberg/rest/RESTCatalogAdapter.java
+++ b/core/src/test/java/org/apache/iceberg/rest/RESTCatalogAdapter.java
@@ -49,6 +49,7 @@ import 
org.apache.iceberg.relocated.com.google.common.collect.Lists;
 import org.apache.iceberg.rest.requests.CommitTransactionRequest;
 import org.apache.iceberg.rest.requests.CreateNamespaceRequest;
 import org.apache.iceberg.rest.requests.CreateTableRequest;
+import org.apache.iceberg.rest.requests.RegisterTableRequest;
 import org.apache.iceberg.rest.requests.RenameTableRequest;
 import org.apache.iceberg.rest.requests.ReportMetricsRequest;
 import org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest;
@@ -126,6 +127,11 @@ public class RESTCatalogAdapter implements RESTClient {
         LoadTableResponse.class),
     LOAD_TABLE(
         HTTPMethod.GET, "v1/namespaces/{namespace}/tables/{table}", null, 
LoadTableResponse.class),
+    REGISTER_TABLE(
+        HTTPMethod.POST,
+        "v1/namespaces/{namespace}/register",
+        RegisterTableRequest.class,
+        LoadTableResponse.class),
     UPDATE_TABLE(
         HTTPMethod.POST,
         "v1/namespaces/{namespace}/tables/{table}",
@@ -345,6 +351,14 @@ public class RESTCatalogAdapter implements RESTClient {
           return castResponse(responseType, CatalogHandlers.loadTable(catalog, 
ident));
         }
 
+      case REGISTER_TABLE:
+        {
+          Namespace namespace = namespaceFromPathVars(vars);
+          RegisterTableRequest request = 
castRequest(RegisterTableRequest.class, body);
+          return castResponse(
+              responseType, CatalogHandlers.registerTable(catalog, namespace, 
request));
+        }
+
       case UPDATE_TABLE:
         {
           TableIdentifier ident = identFromPathVars(vars);
diff --git a/core/src/test/java/org/apache/iceberg/rest/TestResourcePaths.java 
b/core/src/test/java/org/apache/iceberg/rest/TestResourcePaths.java
index b2a5f5073e..e0e61a594e 100644
--- a/core/src/test/java/org/apache/iceberg/rest/TestResourcePaths.java
+++ b/core/src/test/java/org/apache/iceberg/rest/TestResourcePaths.java
@@ -135,4 +135,12 @@ public class TestResourcePaths {
         .isEqualTo("v1/ws/catalog/namespaces/n%1Fs/tables/table");
     
Assertions.assertThat(withoutPrefix.table(ident)).isEqualTo("v1/namespaces/n%1Fs/tables/table");
   }
+
+  @Test
+  public void testRegister() {
+    Namespace ns = Namespace.of("ns");
+    Assertions.assertThat(withPrefix.register(ns))
+        .isEqualTo("v1/ws/catalog/namespaces/ns/register");
+    
Assertions.assertThat(withoutPrefix.register(ns)).isEqualTo("v1/namespaces/ns/register");
+  }
 }
diff --git 
a/core/src/test/java/org/apache/iceberg/rest/requests/TestRegisterTableRequestParser.java
 
b/core/src/test/java/org/apache/iceberg/rest/requests/TestRegisterTableRequestParser.java
new file mode 100644
index 0000000000..9b479d89d7
--- /dev/null
+++ 
b/core/src/test/java/org/apache/iceberg/rest/requests/TestRegisterTableRequestParser.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.rest.requests;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestRegisterTableRequestParser {
+
+  @Test
+  public void nullCheck() {
+    Assertions.assertThatThrownBy(() -> 
RegisterTableRequestParser.toJson(null))
+        .isInstanceOf(IllegalArgumentException.class)
+        .hasMessage("Invalid register table request: null");
+
+    Assertions.assertThatThrownBy(() -> 
RegisterTableRequestParser.fromJson((JsonNode) null))
+        .isInstanceOf(IllegalArgumentException.class)
+        .hasMessage("Cannot parse register table request from null object");
+  }
+
+  @Test
+  public void missingFields() {
+    Assertions.assertThatThrownBy(() -> 
RegisterTableRequestParser.fromJson("{}"))
+        .isInstanceOf(IllegalArgumentException.class)
+        .hasMessage("Cannot parse missing string: name");
+
+    Assertions.assertThatThrownBy(
+            () -> RegisterTableRequestParser.fromJson("{\"name\" : 
\"test_tbl\"}"))
+        .isInstanceOf(IllegalArgumentException.class)
+        .hasMessage("Cannot parse missing string: metadata-location");
+
+    Assertions.assertThatThrownBy(
+            () ->
+                RegisterTableRequestParser.fromJson(
+                    "{\"metadata-location\" : 
\"file://tmp/NS/test_tbl/metadata/00000-d4f60d2f-2ad2-408b-8832-0ed7fbd851ee.metadata.json\"}"))
+        .isInstanceOf(IllegalArgumentException.class)
+        .hasMessage("Cannot parse missing string: name");
+  }
+
+  @Test
+  public void roundTripSerde() {
+    RegisterTableRequest request =
+        ImmutableRegisterTableRequest.builder()
+            .name("table_1")
+            .metadataLocation(
+                
"file://tmp/NS/test_tbl/metadata/00000-d4f60d2f-2ad2-408b-8832-0ed7fbd851ee.metadata.json")
+            .build();
+
+    String expectedJson =
+        "{\n"
+            + "  \"name\" : \"table_1\",\n"
+            + "  \"metadata-location\" : 
\"file://tmp/NS/test_tbl/metadata/00000-d4f60d2f-2ad2-408b-8832-0ed7fbd851ee.metadata.json\"\n"
+            + "}";
+
+    String json = RegisterTableRequestParser.toJson(request, true);
+    assertThat(json).isEqualTo(expectedJson);
+
+    
assertThat(RegisterTableRequestParser.toJson(RegisterTableRequestParser.fromJson(json),
 true))
+        .isEqualTo(expectedJson);
+  }
+}
diff --git a/open-api/rest-catalog-open-api.py 
b/open-api/rest-catalog-open-api.py
index 17fff47f81..2e5465f77a 100644
--- a/open-api/rest-catalog-open-api.py
+++ b/open-api/rest-catalog-open-api.py
@@ -362,6 +362,11 @@ class TableRequirement(BaseModel):
     default_sort_order_id: Optional[int] = Field(None, 
alias='default-sort-order-id')
 
 
+class RegisterTableRequest(BaseModel):
+    name: str
+    metadata_location: str = Field(..., alias='metadata-location')
+
+
 class TokenType(Enum):
     """
     Token type identifier, from RFC 8693 Section 3
diff --git a/open-api/rest-catalog-open-api.yaml 
b/open-api/rest-catalog-open-api.yaml
index 29b19e4a1b..6b61e5b5d4 100644
--- a/open-api/rest-catalog-open-api.yaml
+++ b/open-api/rest-catalog-open-api.yaml
@@ -498,6 +498,58 @@ paths:
         5XX:
           $ref: '#/components/responses/ServerErrorResponse'
 
+  /v1/{prefix}/namespaces/{namespace}/register:
+    parameters:
+      - $ref: '#/components/parameters/prefix'
+      - $ref: '#/components/parameters/namespace'
+
+    post:
+      tags:
+        - Catalog API
+      summary: Register a table in the given namespace using given metadata 
file location
+      description:
+        Register a table using given metadata file location.
+
+      operationId: registerTable
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/RegisterTableRequest'
+      responses:
+        200:
+          $ref: '#/components/responses/LoadTableResponse'
+        400:
+          $ref: '#/components/responses/BadRequestErrorResponse'
+        401:
+          $ref: '#/components/responses/UnauthorizedResponse'
+        403:
+          $ref: '#/components/responses/ForbiddenResponse'
+        404:
+          description: Not Found - The namespace specified does not exist
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ErrorModel'
+              examples:
+                NamespaceNotFound:
+                  $ref: '#/components/examples/NoSuchNamespaceError'
+        409:
+          description: Conflict - The table already exists
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ErrorModel'
+              examples:
+                NamespaceAlreadyExists:
+                  $ref: '#/components/examples/TableAlreadyExistsError'
+        419:
+          $ref: '#/components/responses/AuthenticationTimeoutResponse'
+        503:
+          $ref: '#/components/responses/ServiceUnavailableResponse'
+        5XX:
+          $ref: '#/components/responses/ServerErrorResponse'
+
   /v1/{prefix}/namespaces/{namespace}/tables/{table}:
     parameters:
       - $ref: '#/components/parameters/prefix'
@@ -1913,6 +1965,17 @@ components:
           additionalProperties:
             type: string
 
+    RegisterTableRequest:
+      type: object
+      required:
+        - name
+        - metadata-location
+      properties:
+        name:
+          type: string
+        metadata-location:
+          type: string
+
     TokenType:
       type: string
       enum:

Reply via email to