kbendick commented on a change in pull request #3275:
URL: https://github.com/apache/iceberg/pull/3275#discussion_r779861519



##########
File path: core/src/test/java/org/apache/iceberg/jdbc/TestJdbcCatalog.java
##########
@@ -555,6 +556,66 @@ public void testDropNamespace() {
         "is not empty. 1 tables exist.", () -> 
catalog.dropNamespace(tbl4.namespace()));
   }
 
+  @Test
+  public void testCreateNamespace() {
+    Namespace testNamespace = Namespace.of("testDb", "ns1", "ns2");
+    // Test with null metadata
+    AssertHelpers.assertThrows("Cannot create a namespace with null or empty 
metadata", IllegalArgumentException.class,
+            () -> catalog.createNamespace(testNamespace, null));
+    Assert.assertFalse(catalog.namespaceExists(testNamespace));
+
+    // Test with metadata
+    Map<String, String> testMetadata = ImmutableMap.of("key_1", "value_1", 
"key_2", "value_2", "key_3", "value_3");
+    catalog.createNamespace(testNamespace, testMetadata);
+    Assert.assertTrue(catalog.namespaceExists(testNamespace));
+  }
+
+  @Test
+  public void testSetProperties() {
+    Namespace testNamespace = Namespace.of("testDb", "ns1", "ns2");
+    Map<String, String> testMetadata = ImmutableMap.of("key_1", "value_1", 
"key_2", "value_2",
+            "key_3", "value_3");
+    catalog.createNamespace(testNamespace, testMetadata);
+
+    // Add more properties to set to test insert and update
+    Map<String, String> propertiesToSet = ImmutableMap.of("key_1", 
"new_value_1", "key_2", "new_value_2", "key_3",
+            "new_value_3", "key_4", "value_4", "key_5", "value_5");

Review comment:
       Can we add the properties to set in an order where the keys don't 
already align with the ordering of the existing metadata?
   
   I'm wondering if we need to add a check if we're at the right row number 
when calling the `updatePrperties` function. I think even just removing `key_2` 
from here would trigger this case.

##########
File path: core/src/main/java/org/apache/iceberg/jdbc/JdbcCatalog.java
##########
@@ -250,10 +265,95 @@ public void setConf(Configuration conf) {
     this.conf = conf;
   }
 
+  private boolean insertProperties(Namespace namespace, Map<String, String> 
properties) {
+    String namespaceName = JdbcUtil.namespaceToString(namespace);
+
+    try {
+      int insertedRecords = connections.run(conn -> {
+        String sqlStatement = 
JdbcUtil.insertPropertiesStatement(properties.size());
+
+        try (PreparedStatement sql = conn.prepareStatement(sqlStatement)) {
+          int rowIndex = 0;
+          for (Map.Entry<String, String> keyValue : properties.entrySet()) {
+            sql.setString(rowIndex + 1, catalogName);
+            sql.setString(rowIndex + 2, namespaceName);
+            sql.setString(rowIndex + 3, keyValue.getKey());
+            sql.setString(rowIndex + 4, keyValue.getValue());
+            rowIndex += 4;
+          }
+          return sql.executeUpdate();
+        }
+      });
+
+      if (insertedRecords == properties.size()) {
+        LOG.debug("Successfully inserted {} properties for namespace {}", 
properties, namespaceName);
+        return true;
+      } else {
+        throw new IllegalStateException();
+      }
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new UncheckedInterruptedException(e, "Interrupted in call to 
insertProperties(namespace, properties) " +
+          "Namespace: %s", namespace);
+    } catch (SQLException e) {
+      throw new UncheckedSQLException(e, "Failed to insertProperties to 
namespace: %s in catalog: %s", namespace,

Review comment:
       Nit: We try to keep error messages as human-readable strings as opposed 
to using function names as users likely won't know the names of functions 
anyways (and they can be logged in stack traces).
   
   So possibly this should be `Failed to insert properties to namespace ...`

##########
File path: core/src/main/java/org/apache/iceberg/jdbc/JdbcCatalog.java
##########
@@ -250,10 +265,95 @@ public void setConf(Configuration conf) {
     this.conf = conf;
   }
 
+  private boolean insertProperties(Namespace namespace, Map<String, String> 
properties) {
+    String namespaceName = JdbcUtil.namespaceToString(namespace);
+
+    try {
+      int insertedRecords = connections.run(conn -> {
+        String sqlStatement = 
JdbcUtil.insertPropertiesStatement(properties.size());
+
+        try (PreparedStatement sql = conn.prepareStatement(sqlStatement)) {
+          int rowIndex = 0;
+          for (Map.Entry<String, String> keyValue : properties.entrySet()) {
+            sql.setString(rowIndex + 1, catalogName);
+            sql.setString(rowIndex + 2, namespaceName);
+            sql.setString(rowIndex + 3, keyValue.getKey());
+            sql.setString(rowIndex + 4, keyValue.getValue());
+            rowIndex += 4;
+          }
+          return sql.executeUpdate();
+        }
+      });
+
+      if (insertedRecords == properties.size()) {
+        LOG.debug("Successfully inserted {} properties for namespace {}", 
properties, namespaceName);
+        return true;
+      } else {
+        throw new IllegalStateException();
+      }
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new UncheckedInterruptedException(e, "Interrupted in call to 
insertProperties(namespace, properties) " +
+          "Namespace: %s", namespace);
+    } catch (SQLException e) {
+      throw new UncheckedSQLException(e, "Failed to insertProperties to 
namespace: %s in catalog: %s", namespace,
+          catalogName);
+    }
+  }
+
+  private boolean updateProperties(Namespace namespace, Map<String, String> 
properties) {
+    String namespaceName = JdbcUtil.namespaceToString(namespace);
+
+    try {
+      int updatedRecords = connections.run(conn -> {
+        String sqlStatement = 
JdbcUtil.updatePropertiesStatement(properties.size());
+
+        try (PreparedStatement sql = conn.prepareStatement(sqlStatement)) {
+          int rowIndex = 0;
+          for (Map.Entry<String, String> keyValue : properties.entrySet()) {
+            sql.setString(rowIndex + 1, keyValue.getKey());
+            sql.setString(rowIndex + 2, keyValue.getValue());
+            rowIndex += 2;
+          }
+          sql.setString(rowIndex + 1, catalogName);
+          sql.setString(rowIndex + 2, namespaceName);
+          rowIndex += 2;
+          for (String key : properties.keySet()) {
+            sql.setString(rowIndex + 1, key);
+            rowIndex += 1;
+          }
+          LOG.info("Final log string {}", sql);
+          return sql.executeUpdate();
+        }
+      });
+
+      if (updatedRecords == properties.size()) {
+        LOG.debug("Successfully updated {} to new namespace: {}", properties, 
namespaceName);
+        return true;
+      } else {
+        throw new IllegalStateException();
+      }
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new UncheckedInterruptedException(e,
+          "Interrupted in call to updateProperties(namespace, properties) 
Namespace: %s", namespace);
+    } catch (SQLException e) {
+      throw new UncheckedSQLException(e, "Failed to updateProperties to 
namespace: %s in catalog: %s", namespace,
+              catalogName);
+    }
+  }
+
   @Override
   public void createNamespace(Namespace namespace, Map<String, String> 
metadata) {
-    throw new UnsupportedOperationException("Cannot create namespace " + 
namespace +
-        ": createNamespace is not supported");
+    if (namespaceExists(namespace)) {
+      throw new AlreadyExistsException("Namespace already exists: %s", 
namespace);
+    }
+
+    if (metadata == null || metadata.isEmpty()) {
+      throw new IllegalArgumentException("Cannot create a namespace with null 
or empty metadata");
+    }

Review comment:
       After looking at the reasoning for disallowing empty metadata, 
https://github.com/apache/iceberg/pull/3275/files#r741604136, should we add a 
short comment above to indicate why this can't be empty?
   
   Or, since we're using the metadata to indicate that a record exists, if 
users don't supply metadata, should we simply place some sort of value in there 
for them (possible choices I can think of might be a creation timestamp). This 
way users would be able to create namespaces without explicitly passing in 
metadata.
   
   Returning to this PR after a while so forgive me if there's been a reason 
covered that this is a bad idea already that I'm forgetting 😄 

##########
File path: core/src/main/java/org/apache/iceberg/jdbc/JdbcCatalog.java
##########
@@ -250,10 +265,93 @@ public void setConf(Configuration conf) {
     this.conf = conf;
   }
 
+  private boolean insertProperties(Namespace namespace, Map<String, String> 
properties) {
+    String namespaceName = JdbcUtil.namespaceToString(namespace);
+
+    try {
+      int insertedRecords = connections.run(conn -> {
+        String sqlStatement = JdbcUtil.insertPropertiesStatement(properties);
+
+        try (PreparedStatement sql = conn.prepareStatement(sqlStatement)) {
+          int rowIndex = 0;
+          for (Map.Entry<String, String> keyValue : properties.entrySet()) {
+            sql.setString(++rowIndex, catalogName);
+            sql.setString(++rowIndex, namespaceName);
+            sql.setString(++rowIndex, keyValue.getKey());
+            sql.setString(++rowIndex, keyValue.getValue());
+          }
+          return sql.executeUpdate();
+        }
+      });
+
+      if (insertedRecords == properties.size()) {
+        LOG.debug("Successfully committed to new namespace: {}", 
namespaceName);
+        return true;
+      } else {
+        throw new CommitFailedException("Failed to insertProperties to 
namespace %s in catalog %s", namespaceName,
+                catalogName);
+      }
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new UncheckedInterruptedException(e,
+              "Interrupted in call to insertProperties(namespace, properties) 
Namespace: %s", namespace);
+    } catch (SQLException e) {
+      throw new UncheckedSQLException(e, "Failed to insertProperties to 
namespace: %s in catalog: %s", namespace,
+              catalogName);
+    }
+  }
+
+  private boolean updateProperties(Namespace namespace, Map<String, String> 
properties) {

Review comment:
       I would personally say if there's no practical difference between the 
two, it would be ok to make them into one function.
   
   Possibly there could be one outer function, that contains all of the shared 
logic (like the interrupted exception and SQLException checks). And then two 
different functions that wrap that function with a code block for the building 
of the correct prepared statement. This might make the changes a bit easier to 
digest and be cleaner, but it's fine as it is otherwise.

##########
File path: core/src/main/java/org/apache/iceberg/jdbc/JdbcCatalog.java
##########
@@ -340,14 +440,119 @@ public boolean dropNamespace(Namespace namespace) throws 
NamespaceNotEmptyExcept
   @Override
   public boolean setProperties(Namespace namespace, Map<String, String> 
properties) throws
       NoSuchNamespaceException {
-    throw new UnsupportedOperationException(
-        "Cannot set properties " + namespace + " : setProperties is not 
supported");
+    if (!namespaceExists(namespace)) {
+      throw new NoSuchNamespaceException("Namespace does not exist: %s", 
namespace);
+    }
+
+    if (properties == null || properties.isEmpty()) {
+      throw new IllegalArgumentException("Cannot setProperties to a namespace 
with null or empty properties");
+    }

Review comment:
       Nit: This could probably be `Preconditions.checkArgument` or some 
variant of that.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]



---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to