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

jshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 5d00b538d5 [#6750] improvement(core): property framework supports 
prefix (#6751)
5d00b538d5 is described below

commit 5d00b538d5e8c4079c200889c06439627392fcc4
Author: mchades <[email protected]>
AuthorDate: Thu Apr 3 13:02:58 2025 +0800

    [#6750] improvement(core): property framework supports prefix (#6751)
    
    ### What changes were proposed in this pull request?
    
    property framework supports prefix
    
    ### Why are the changes needed?
    
    some properties with fixed prefixes maybe immutable or required and need
    to be verified.
    
    Fix: #6750
    
    ### Does this PR introduce _any_ user-facing change?
    
    no
    
    ### How was this patch tested?
    
    tests added
---
 .../hadoop/HadoopFilesetPropertiesMetadata.java    |  10 ++
 .../hadoop/integration/test/HadoopCatalogIT.java   |  27 ++++
 .../gravitino/catalog/hive/TestHiveCatalog.java    |   4 +-
 .../kafka/integration/test/CatalogKafkaIT.java     |   6 +-
 .../hudi/integration/test/HudiCatalogHMSIT.java    |  16 +-
 .../catalog/PropertiesMetadataHelpers.java         |  34 ++--
 .../gravitino/connector/PropertiesMetadata.java    | 105 ++++++++++--
 .../apache/gravitino/connector/PropertyEntry.java  | 109 ++++++++++++-
 .../java/org/apache/gravitino/TestCatalog.java     |  86 ++++++++--
 .../gravitino/catalog/TestCatalogManager.java      | 178 +++++++++++++++++----
 .../catalog/TestCatalogNormalizeDispatcher.java    |  12 +-
 .../catalog/TestFilesetOperationDispatcher.java    |   4 +-
 .../catalog/TestModelOperationDispatcher.java      |   4 +-
 .../gravitino/catalog/TestOperationDispatcher.java |   7 +-
 .../catalog/TestSchemaOperationDispatcher.java     |   4 +-
 .../catalog/TestTableOperationDispatcher.java      |   4 +-
 .../catalog/TestTopicOperationDispatcher.java      |   9 +-
 docs/hadoop-catalog.md                             |  26 +--
 18 files changed, 536 insertions(+), 109 deletions(-)

diff --git 
a/catalogs/catalog-hadoop/src/main/java/org/apache/gravitino/catalog/hadoop/HadoopFilesetPropertiesMetadata.java
 
b/catalogs/catalog-hadoop/src/main/java/org/apache/gravitino/catalog/hadoop/HadoopFilesetPropertiesMetadata.java
index 1bdf224671..2a43de62e1 100644
--- 
a/catalogs/catalog-hadoop/src/main/java/org/apache/gravitino/catalog/hadoop/HadoopFilesetPropertiesMetadata.java
+++ 
b/catalogs/catalog-hadoop/src/main/java/org/apache/gravitino/catalog/hadoop/HadoopFilesetPropertiesMetadata.java
@@ -18,6 +18,7 @@
  */
 package org.apache.gravitino.catalog.hadoop;
 
+import static org.apache.gravitino.file.Fileset.LOCATION_PLACEHOLDER_PREFIX;
 import static org.apache.gravitino.file.Fileset.RESERVED_CATALOG_PLACEHOLDER;
 import static org.apache.gravitino.file.Fileset.RESERVED_FILESET_PLACEHOLDER;
 import static org.apache.gravitino.file.Fileset.RESERVED_SCHEMA_PLACEHOLDER;
@@ -54,6 +55,15 @@ public class HadoopFilesetPropertiesMetadata extends 
BasePropertiesMetadata {
                 RESERVED_FILESET_PLACEHOLDER,
                 "The placeholder will be replaced to fileset name in the 
location",
                 true /* hidden */))
+        .put(
+            LOCATION_PLACEHOLDER_PREFIX,
+            PropertyEntry.stringImmutablePropertyPrefixEntry(
+                LOCATION_PLACEHOLDER_PREFIX,
+                "The prefix of fileset placeholder property",
+                false /* required */,
+                null /* default value */,
+                false /* hidden */,
+                false /* reserved */))
         .putAll(KerberosConfig.KERBEROS_PROPERTY_ENTRIES)
         .putAll(AuthenticationConfig.AUTHENTICATION_PROPERTY_ENTRIES)
         .putAll(CredentialConfig.CREDENTIAL_PROPERTY_ENTRIES);
diff --git 
a/catalogs/catalog-hadoop/src/test/java/org/apache/gravitino/catalog/hadoop/integration/test/HadoopCatalogIT.java
 
b/catalogs/catalog-hadoop/src/test/java/org/apache/gravitino/catalog/hadoop/integration/test/HadoopCatalogIT.java
index 6c438506a5..05c166df13 100644
--- 
a/catalogs/catalog-hadoop/src/test/java/org/apache/gravitino/catalog/hadoop/integration/test/HadoopCatalogIT.java
+++ 
b/catalogs/catalog-hadoop/src/test/java/org/apache/gravitino/catalog/hadoop/integration/test/HadoopCatalogIT.java
@@ -18,6 +18,7 @@
  */
 package org.apache.gravitino.catalog.hadoop.integration.test;
 
+import static org.apache.gravitino.file.Fileset.LOCATION_PLACEHOLDER_PREFIX;
 import static org.apache.gravitino.file.Fileset.Type.MANAGED;
 
 import com.google.common.collect.ImmutableMap;
@@ -255,6 +256,32 @@ public class HadoopCatalogIT extends BaseIT {
     Assertions.assertEquals(MANAGED, fileset3.type(), "fileset type should be 
MANAGED by default");
   }
 
+  @Test
+  public void testAlterFileset() {
+    // create fileset with placeholder in storage location
+    String filesetName = "test_alter_fileset";
+    String storageLocation = storageLocation(filesetName) + "/{{user}}";
+    String placeholderKey = LOCATION_PLACEHOLDER_PREFIX + "user";
+    createFileset(
+        filesetName, "comment", MANAGED, storageLocation, 
ImmutableMap.of(placeholderKey, "test"));
+
+    // alter fileset
+    Exception exception =
+        Assertions.assertThrows(
+            IllegalArgumentException.class,
+            () ->
+                catalog
+                    .asFilesetCatalog()
+                    .alterFileset(
+                        NameIdentifier.of(schemaName, filesetName),
+                        FilesetChange.setProperty(placeholderKey, "test2")));
+    Assertions.assertTrue(
+        exception
+            .getMessage()
+            .contains("Property placeholder-user is immutable or reserved, 
cannot be set"),
+        exception.getMessage());
+  }
+
   @Test
   public void testCreateFilesetWithChinese() throws IOException {
     // create fileset
diff --git 
a/catalogs/catalog-hive/src/test/java/org/apache/gravitino/catalog/hive/TestHiveCatalog.java
 
b/catalogs/catalog-hive/src/test/java/org/apache/gravitino/catalog/hive/TestHiveCatalog.java
index ddf7616318..910fe2db70 100644
--- 
a/catalogs/catalog-hive/src/test/java/org/apache/gravitino/catalog/hive/TestHiveCatalog.java
+++ 
b/catalogs/catalog-hive/src/test/java/org/apache/gravitino/catalog/hive/TestHiveCatalog.java
@@ -125,6 +125,8 @@ public class TestHiveCatalog extends 
MiniHiveMetastoreService {
         throwable
             .getMessage()
             .contains(
-                String.format("Properties are required and must be set: [%s]", 
METASTORE_URIS)));
+                String.format(
+                    "Properties or property prefixes are required and must be 
set: [%s]",
+                    METASTORE_URIS)));
   }
 }
diff --git 
a/catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/integration/test/CatalogKafkaIT.java
 
b/catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/integration/test/CatalogKafkaIT.java
index 52ebb8dab0..292325a435 100644
--- 
a/catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/integration/test/CatalogKafkaIT.java
+++ 
b/catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/integration/test/CatalogKafkaIT.java
@@ -218,7 +218,8 @@ public class CatalogKafkaIT extends BaseIT {
     Assertions.assertTrue(
         exception
             .getMessage()
-            .contains("Properties are required and must be set: 
[bootstrap.servers]"));
+            .contains(
+                "Properties or property prefixes are required and must be set: 
[bootstrap.servers]"));
 
     exception =
         Assertions.assertThrows(
@@ -233,7 +234,8 @@ public class CatalogKafkaIT extends BaseIT {
     Assertions.assertTrue(
         exception
             .getMessage()
-            .contains("Properties are required and must be set: 
[bootstrap.servers]"));
+            .contains(
+                "Properties or property prefixes are required and must be set: 
[bootstrap.servers]"));
 
     // Test BOOTSTRAP_SERVERS that cannot be linked
     String catalogName2 = GravitinoITUtils.genRandomName("test_catalog");
diff --git 
a/catalogs/catalog-lakehouse-hudi/src/test/java/org/apache/gravitino/catalog/lakehouse/hudi/integration/test/HudiCatalogHMSIT.java
 
b/catalogs/catalog-lakehouse-hudi/src/test/java/org/apache/gravitino/catalog/lakehouse/hudi/integration/test/HudiCatalogHMSIT.java
index 9fc1c81b5f..7b2800f0f9 100644
--- 
a/catalogs/catalog-lakehouse-hudi/src/test/java/org/apache/gravitino/catalog/lakehouse/hudi/integration/test/HudiCatalogHMSIT.java
+++ 
b/catalogs/catalog-lakehouse-hudi/src/test/java/org/apache/gravitino/catalog/lakehouse/hudi/integration/test/HudiCatalogHMSIT.java
@@ -113,7 +113,13 @@ public class HudiCatalogHMSIT extends BaseIT {
     Assertions.assertTrue(
         exception
             .getMessage()
-            .contains("Properties are required and must be set: 
[catalog-backend, uri]"),
+            .contains("Properties or property prefixes are required and must 
be set"),
+        "Unexpected exception message: " + exception.getMessage());
+    Assertions.assertTrue(
+        exception.getMessage().contains("catalog-backend"),
+        "Unexpected exception message: " + exception.getMessage());
+    Assertions.assertTrue(
+        exception.getMessage().contains("uri"),
         "Unexpected exception message: " + exception.getMessage());
 
     // test testConnection exception
@@ -130,7 +136,13 @@ public class HudiCatalogHMSIT extends BaseIT {
     Assertions.assertTrue(
         exception
             .getMessage()
-            .contains("Properties are required and must be set: 
[catalog-backend, uri]"),
+            .contains("Properties or property prefixes are required and must 
be set"),
+        "Unexpected exception message: " + exception.getMessage());
+    Assertions.assertTrue(
+        exception.getMessage().contains("catalog-backend"),
+        "Unexpected exception message: " + exception.getMessage());
+    Assertions.assertTrue(
+        exception.getMessage().contains("uri"),
         "Unexpected exception message: " + exception.getMessage());
 
     // test testConnection
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/PropertiesMetadataHelpers.java
 
b/core/src/main/java/org/apache/gravitino/catalog/PropertiesMetadataHelpers.java
index 54320c325c..c465b2a4cd 100644
--- 
a/core/src/main/java/org/apache/gravitino/catalog/PropertiesMetadataHelpers.java
+++ 
b/core/src/main/java/org/apache/gravitino/catalog/PropertiesMetadataHelpers.java
@@ -54,17 +54,23 @@ public class PropertiesMetadataHelpers {
             .collect(Collectors.toList());
     Preconditions.checkArgument(
         reservedProperties.isEmpty(),
-        "Properties are reserved and cannot be set: %s",
+        "Properties or property prefixes are reserved and cannot be set: %s",
         reservedProperties);
 
     List<String> absentProperties =
-        propertiesMetadata.propertyEntries().keySet().stream()
-            .filter(propertiesMetadata::isRequiredProperty)
-            .filter(k -> !properties.containsKey(k))
+        propertiesMetadata.propertyEntries().values().stream()
+            .filter(PropertyEntry::isRequired)
+            .filter(
+                e ->
+                    (!e.isPrefix() && !properties.containsKey(e.getName()))
+                        || (e.isPrefix()
+                            && properties.keySet().stream()
+                                .noneMatch(k -> k.startsWith(e.getName()))))
+            .map(PropertyEntry::getName)
             .collect(Collectors.toList());
     Preconditions.checkArgument(
         absentProperties.isEmpty(),
-        "Properties are required and must be set: %s",
+        "Properties or property prefixes are required and must be set: %s",
         absentProperties);
 
     // use decode function to validate the property values
@@ -72,7 +78,7 @@ public class PropertiesMetadataHelpers {
       String key = entry.getKey();
       String value = entry.getValue();
       if (propertiesMetadata.containsProperty(key)) {
-        checkValueFormat(key, value, 
propertiesMetadata.propertyEntries().get(key)::decode);
+        checkValueFormat(key, value, 
propertiesMetadata.getPropertyEntry(key)::decode);
       }
     }
   }
@@ -82,21 +88,29 @@ public class PropertiesMetadataHelpers {
       Map<String, String> upserts,
       Map<String, String> deletes) {
     for (Map.Entry<String, String> entry : upserts.entrySet()) {
-      PropertyEntry<?> propertyEntry = 
propertiesMetadata.propertyEntries().get(entry.getKey());
+      if (!propertiesMetadata.containsProperty(entry.getKey())) {
+        continue;
+      }
+
+      PropertyEntry<?> propertyEntry = 
propertiesMetadata.getPropertyEntry(entry.getKey());
       if (Objects.nonNull(propertyEntry)) {
         Preconditions.checkArgument(
             !propertyEntry.isImmutable() && !propertyEntry.isReserved(),
-            "Property " + propertyEntry.getName() + " is immutable or 
reserved, cannot be set");
+            "Property " + entry.getKey() + " is immutable or reserved, cannot 
be set");
         checkValueFormat(entry.getKey(), entry.getValue(), 
propertyEntry::decode);
       }
     }
 
     for (Map.Entry<String, String> entry : deletes.entrySet()) {
-      PropertyEntry<?> propertyEntry = 
propertiesMetadata.propertyEntries().get(entry.getKey());
+      if (!propertiesMetadata.containsProperty(entry.getKey())) {
+        continue;
+      }
+
+      PropertyEntry<?> propertyEntry = 
propertiesMetadata.getPropertyEntry(entry.getKey());
       if (Objects.nonNull(propertyEntry)) {
         Preconditions.checkArgument(
             !propertyEntry.isImmutable() && !propertyEntry.isReserved(),
-            "Property " + propertyEntry.getName() + " is immutable or 
reserved, cannot be deleted");
+            "Property " + entry.getKey() + " is immutable or reserved, cannot 
be deleted");
       }
     }
   }
diff --git 
a/core/src/main/java/org/apache/gravitino/connector/PropertiesMetadata.java 
b/core/src/main/java/org/apache/gravitino/connector/PropertiesMetadata.java
index d4778b2ff9..46df3dd86f 100644
--- a/core/src/main/java/org/apache/gravitino/connector/PropertiesMetadata.java
+++ b/core/src/main/java/org/apache/gravitino/connector/PropertiesMetadata.java
@@ -18,7 +18,9 @@
  */
 package org.apache.gravitino.connector;
 
+import java.util.Comparator;
 import java.util.Map;
+import java.util.Optional;
 import org.apache.gravitino.annotation.Evolving;
 
 /** The PropertiesMetadata class is responsible for managing property 
metadata. */
@@ -35,8 +37,13 @@ public interface PropertiesMetadata {
    * @return true if the property is existed and reserved, false otherwise.
    */
   default boolean isReservedProperty(String propertyName) {
-    return propertyEntries().containsKey(propertyName)
-        && propertyEntries().get(propertyName).isReserved();
+    // First check non-prefix exact match
+    if 
(getNonPrefixEntry(propertyName).map(PropertyEntry::isReserved).orElse(false)) {
+      return true;
+    }
+
+    // Then check property prefixes
+    return 
getPropertyPrefixEntry(propertyName).map(PropertyEntry::isReserved).orElse(false);
   }
 
   /**
@@ -46,8 +53,13 @@ public interface PropertiesMetadata {
    * @return true if the property is existed and required, false otherwise.
    */
   default boolean isRequiredProperty(String propertyName) {
-    return propertyEntries().containsKey(propertyName)
-        && propertyEntries().get(propertyName).isRequired();
+    // First check non-prefix exact match
+    if 
(getNonPrefixEntry(propertyName).map(PropertyEntry::isRequired).orElse(false)) {
+      return true;
+    }
+
+    // Then check property prefixes
+    return 
getPropertyPrefixEntry(propertyName).map(PropertyEntry::isRequired).orElse(false);
   }
 
   /**
@@ -57,8 +69,13 @@ public interface PropertiesMetadata {
    * @return true if the property is existed and immutable, false otherwise.
    */
   default boolean isImmutableProperty(String propertyName) {
-    return propertyEntries().containsKey(propertyName)
-        && propertyEntries().get(propertyName).isImmutable();
+    // First check non-prefix exact match
+    if 
(getNonPrefixEntry(propertyName).map(PropertyEntry::isImmutable).orElse(false)) 
{
+      return true;
+    }
+
+    // Then check property prefixes
+    return 
getPropertyPrefixEntry(propertyName).map(PropertyEntry::isImmutable).orElse(false);
   }
 
   /**
@@ -68,13 +85,19 @@ public interface PropertiesMetadata {
    * @return true if the property is existed and hidden, false otherwise.
    */
   default boolean isHiddenProperty(String propertyName) {
-    return propertyEntries().containsKey(propertyName)
-        && propertyEntries().get(propertyName).isHidden();
+    // First check non-prefix exact match
+    if 
(getNonPrefixEntry(propertyName).map(PropertyEntry::isHidden).orElse(false)) {
+      return true;
+    }
+
+    // Then check property prefixes
+    return 
getPropertyPrefixEntry(propertyName).map(PropertyEntry::isHidden).orElse(false);
   }
 
   /** @return true if the property is existed, false otherwise. */
   default boolean containsProperty(String propertyName) {
-    return propertyEntries().containsKey(propertyName);
+    return getNonPrefixEntry(propertyName).isPresent()
+        || getPropertyPrefixEntry(propertyName).isPresent();
   }
 
   /**
@@ -85,14 +108,12 @@ public interface PropertiesMetadata {
    * @return the value object of the property.
    */
   default Object getOrDefault(Map<String, String> properties, String 
propertyName) {
-    if (!containsProperty(propertyName)) {
-      throw new IllegalArgumentException("Property is not defined: " + 
propertyName);
-    }
+    PropertyEntry<?> propertyEntry = getPropertyEntry(propertyName);
 
     if (properties != null && properties.containsKey(propertyName)) {
-      return 
propertyEntries().get(propertyName).decode(properties.get(propertyName));
+      return propertyEntry.decode(properties.get(propertyName));
     }
-    return propertyEntries().get(propertyName).getDefaultValue();
+    return propertyEntry.getDefaultValue();
   }
 
   /**
@@ -102,10 +123,64 @@ public interface PropertiesMetadata {
    * @return the default value object of the property.
    */
   default Object getDefaultValue(String propertyName) {
+    PropertyEntry<?> propertyEntry = getPropertyEntry(propertyName);
+
+    return propertyEntry.getDefaultValue();
+  }
+
+  /**
+   * Get the property entry of the property. The non-prefix property entry 
will be returned if
+   * exists, otherwise the longest prefix property entry will be returned.
+   *
+   * <p>For example, if there are two property prefix entries "foo." and 
"foo.bar.", and the
+   * property name is "foo.bar.baz", the entry for "foo.bar." will be 
returned. If the property
+   * entry "foo.bar.baz" is defined, it will be returned instead.
+   *
+   * @param propertyName The name of the property.
+   * @return the property entry object of the property.
+   * @throws IllegalArgumentException if the property is not defined.
+   */
+  default PropertyEntry<?> getPropertyEntry(String propertyName) throws 
IllegalArgumentException {
     if (!containsProperty(propertyName)) {
       throw new IllegalArgumentException("Property is not defined: " + 
propertyName);
     }
+    return getNonPrefixEntry(propertyName).isPresent()
+        ? getNonPrefixEntry(propertyName).get()
+        : getPropertyPrefixEntry(propertyName).get();
+  }
+
+  /**
+   * Get the property prefix entry of the property. If there are multiple 
property prefix entries
+   * matching the property name, the longest prefix entry will be returned.
+   *
+   * <p>For example, if there are two property prefix entries "foo." and 
"foo.bar.", and the
+   * property name is "foo.bar.baz", the entry for "foo.bar." will be returned.
+   *
+   * @param propertyName The name of the property.
+   * @return an Optional containing the property prefix entry if it exists, or 
empty Optional
+   *     otherwise.
+   */
+  default Optional<PropertyEntry<?>> getPropertyPrefixEntry(String 
propertyName) {
+    return propertyEntries().entrySet().stream()
+        .filter(e -> e.getValue().isPrefix() && 
propertyName.startsWith(e.getKey()))
+        // get the longest prefix property
+        .max(Map.Entry.comparingByKey(Comparator.comparingInt(String::length)))
+        .map(Map.Entry::getValue);
+  }
 
-    return propertyEntries().get(propertyName).getDefaultValue();
+  /**
+   * Get the non-prefix property entry of the property. That is, the property 
entry that is not a
+   * prefix.
+   *
+   * @param propertyName The name of the property.
+   * @return an Optional containing the non-prefix property entry if it 
exists, or empty Optional
+   *     otherwise.
+   */
+  default Optional<PropertyEntry<?>> getNonPrefixEntry(String propertyName) {
+    if (propertyEntries().containsKey(propertyName)
+        && !propertyEntries().get(propertyName).isPrefix()) {
+      return Optional.of(propertyEntries().get(propertyName));
+    }
+    return Optional.empty();
   }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/connector/PropertyEntry.java 
b/core/src/main/java/org/apache/gravitino/connector/PropertyEntry.java
index a32c2fff21..fff77ae677 100644
--- a/core/src/main/java/org/apache/gravitino/connector/PropertyEntry.java
+++ b/core/src/main/java/org/apache/gravitino/connector/PropertyEntry.java
@@ -40,6 +40,7 @@ public final class PropertyEntry<T> {
   private final Function<T, String> encoder;
   private final boolean hidden;
   private final boolean reserved;
+  private final boolean prefix;
 
   /**
    * @param name The name of the property
@@ -66,6 +67,46 @@ public final class PropertyEntry<T> {
       Function<T, String> encoder,
       boolean hidden,
       boolean reserved) {
+    this(
+        name,
+        description,
+        required,
+        immutable,
+        javaType,
+        defaultValue,
+        decoder,
+        encoder,
+        hidden,
+        reserved,
+        false);
+  }
+
+  /**
+   * @param name The name of the property
+   * @param description Describe the purpose of this property
+   * @param required Whether this property is required. If true, the property 
must be set when
+   *     creating a table
+   * @param immutable Whether this property is immutable. If true, the 
property cannot be changed by
+   *     user after the table is created
+   * @param javaType The java type of the property
+   * @param defaultValue Non-required property can have a default value
+   * @param decoder Decode the string value to the java type
+   * @param encoder Encode the java type to the string value
+   * @param hidden Whether this property is hidden from user, such as password
+   * @param reserved This property is reserved and cannot be set by user
+   */
+  private PropertyEntry(
+      String name,
+      String description,
+      boolean required,
+      boolean immutable,
+      Class<T> javaType,
+      T defaultValue,
+      Function<String, T> decoder,
+      Function<T, String> encoder,
+      boolean hidden,
+      boolean reserved,
+      boolean prefix) {
     Preconditions.checkArgument(StringUtils.isNotBlank(name), "name cannot be 
null or empty");
     Preconditions.checkArgument(
         StringUtils.isNotBlank(description), "description cannot be null or 
empty");
@@ -88,6 +129,7 @@ public final class PropertyEntry<T> {
     this.encoder = encoder;
     this.hidden = hidden;
     this.reserved = reserved;
+    this.prefix = prefix;
   }
 
   public static class Builder<T> {
@@ -102,6 +144,7 @@ public final class PropertyEntry<T> {
     private Function<T, String> encoder;
     private boolean hidden;
     private boolean reserved;
+    private boolean prefix;
 
     public Builder<T> withName(String name) {
       this.name = name;
@@ -153,8 +196,13 @@ public final class PropertyEntry<T> {
       return this;
     }
 
+    public Builder<T> withPrefix(boolean prefix) {
+      this.prefix = prefix;
+      return this;
+    }
+
     public PropertyEntry<T> build() {
-      return new PropertyEntry<T>(
+      return new PropertyEntry<>(
           name,
           description,
           required,
@@ -164,7 +212,8 @@ public final class PropertyEntry<T> {
           decoder,
           encoder,
           hidden,
-          reserved);
+          reserved,
+          prefix);
     }
   }
 
@@ -172,6 +221,29 @@ public final class PropertyEntry<T> {
     return decoder.apply(value);
   }
 
+  public static PropertyEntry<String> stringPropertyPrefixEntry(
+      String name,
+      String description,
+      boolean required,
+      boolean immutable,
+      String defaultValue,
+      boolean hidden,
+      boolean reserved) {
+    return new Builder<String>()
+        .withName(name)
+        .withDescription(description)
+        .withRequired(required)
+        .withImmutable(immutable)
+        .withJavaType(String.class)
+        .withDefaultValue(defaultValue)
+        .withDecoder(Function.identity())
+        .withEncoder(Function.identity())
+        .withHidden(hidden)
+        .withReserved(reserved)
+        .withPrefix(true)
+        .build();
+  }
+
   public static PropertyEntry<String> stringPropertyEntry(
       String name,
       String description,
@@ -354,6 +426,39 @@ public final class PropertyEntry<T> {
     return stringPropertyEntry(name, description, required, true, 
defaultValue, hidden, reserved);
   }
 
+  public static PropertyEntry<String> stringImmutablePropertyPrefixEntry(
+      String name,
+      String description,
+      boolean required,
+      String defaultValue,
+      boolean hidden,
+      boolean reserved) {
+    return stringPropertyPrefixEntry(
+        name, description, required, true /* immutable */, defaultValue, 
hidden, reserved);
+  }
+
+  public static PropertyEntry<String> stringRequiredPropertyPrefixEntry(
+      String name,
+      String description,
+      boolean immutable,
+      String defaultValue,
+      boolean hidden,
+      boolean reserved) {
+    return stringPropertyPrefixEntry(
+        name, description, true /* required */, immutable, defaultValue, 
hidden, reserved);
+  }
+
+  public static PropertyEntry<String> stringOptionalPropertyPrefixEntry(
+      String name,
+      String description,
+      boolean immutable,
+      String defaultValue,
+      boolean hidden,
+      boolean reserved) {
+    return stringPropertyPrefixEntry(
+        name, description, false /* required */, immutable, defaultValue, 
hidden, reserved);
+  }
+
   public static <T extends Enum<T>> PropertyEntry<T> enumPropertyEntry(
       String name,
       String description,
diff --git a/core/src/test/java/org/apache/gravitino/TestCatalog.java 
b/core/src/test/java/org/apache/gravitino/TestCatalog.java
index 420396559d..fe353b94e9 100644
--- a/core/src/test/java/org/apache/gravitino/TestCatalog.java
+++ b/core/src/test/java/org/apache/gravitino/TestCatalog.java
@@ -39,6 +39,15 @@ public class TestCatalog extends BaseCatalog<TestCatalog> {
   private static final TestFilesetPropertiesMetadata 
FILESET_PROPERTIES_METADATA =
       new TestFilesetPropertiesMetadata();
 
+  public static final String PROPERTY_KEY1 = "key1";
+  public static final String PROPERTY_KEY2 = "key2";
+  public static final String PROPERTY_KEY3 = "key3";
+  public static final String PROPERTY_KEY4 = "key4";
+  public static final String PROPERTY_RESERVED_KEY = "reserved_key";
+  public static final String PROPERTY_HIDDEN_KEY = "hidden_key";
+  public static final String PROPERTY_KEY5_PREFIX = "key5-";
+  public static final String PROPERTY_KEY6_PREFIX = "key6-";
+
   public TestCatalog() {}
 
   @Override
@@ -73,14 +82,27 @@ public class TestCatalog extends BaseCatalog<TestCatalog> {
       protected Map<String, PropertyEntry<?>> specificPropertyEntries() {
         return ImmutableMap.<String, PropertyEntry<?>>builder()
             .put(
-                "key1",
-                PropertyEntry.stringPropertyEntry("key1", "value1", true, 
true, null, false, false))
+                PROPERTY_KEY1,
+                PropertyEntry.stringPropertyEntry(
+                    PROPERTY_KEY1,
+                    "value1" /* description */,
+                    true /* required */,
+                    true /* immutable */,
+                    null /* default value*/,
+                    false /* hidden */,
+                    false /* reserved */))
             .put(
-                "key2",
+                PROPERTY_KEY2,
                 PropertyEntry.stringPropertyEntry(
-                    "key2", "value2", true, false, null, false, false))
+                    PROPERTY_KEY2,
+                    "value2" /* description */,
+                    true /* required */,
+                    false /* immutable */,
+                    null /* default value*/,
+                    false /* hidden */,
+                    false /* reserved */))
             .put(
-                "key3",
+                PROPERTY_KEY3,
                 new PropertyEntry.Builder<Integer>()
                     .withDecoder(Integer::parseInt)
                     .withEncoder(Object::toString)
@@ -91,30 +113,60 @@ public class TestCatalog extends BaseCatalog<TestCatalog> {
                     .withImmutable(true)
                     .withJavaType(Integer.class)
                     .withRequired(false)
-                    .withName("key3")
+                    .withName(PROPERTY_KEY3)
                     .build())
             .put(
-                "key4",
+                PROPERTY_KEY4,
                 PropertyEntry.stringPropertyEntry(
-                    "key4", "value4", false, false, "value4", false, false))
+                    PROPERTY_KEY4, "value4", false, false, "value4", false, 
false))
             .put(
-                "reserved_key",
+                PROPERTY_RESERVED_KEY,
                 PropertyEntry.stringPropertyEntry(
-                    "reserved_key", "reserved_key", false, true, 
"reserved_value", false, true))
+                    PROPERTY_RESERVED_KEY,
+                    "reserved_key" /* description */,
+                    false /* required */,
+                    true /* immutable */,
+                    "reserved_value" /* default value*/,
+                    false /* hidden */,
+                    true /* reserved */))
             .put(
-                "hidden_key",
+                PROPERTY_HIDDEN_KEY,
                 PropertyEntry.stringPropertyEntry(
-                    "hidden_key", "hidden_key", false, false, "hidden_value", 
true, false))
+                    PROPERTY_HIDDEN_KEY,
+                    "hidden_key" /* description */,
+                    false /* required */,
+                    false /* immutable */,
+                    "hidden_value" /* default value*/,
+                    true /* hidden */,
+                    false /* reserved */))
             .put(
                 FAIL_CREATE,
                 PropertyEntry.booleanPropertyEntry(
                     FAIL_CREATE,
                     "Whether an exception needs to be thrown on creation",
-                    false,
-                    false,
-                    false,
-                    false,
-                    false))
+                    false /* required */,
+                    false /* immutable */,
+                    false /* default value*/,
+                    false /* hidden */,
+                    false /* reserved */))
+            .put(
+                PROPERTY_KEY5_PREFIX,
+                PropertyEntry.stringRequiredPropertyPrefixEntry(
+                    PROPERTY_KEY5_PREFIX,
+                    "property with prefix 'key5-'",
+                    false /* immutable */,
+                    null /* default value*/,
+                    false /* hidden */,
+                    false /* reserved */))
+            .put(
+                PROPERTY_KEY6_PREFIX,
+                PropertyEntry.stringImmutablePropertyPrefixEntry(
+                    PROPERTY_KEY6_PREFIX,
+                    "property with prefix 'key6-'",
+                    false /* required */,
+                    null /* default value*/,
+                    false /* hidden */,
+                    false /* reserved */))
             .build();
       }
     };
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java 
b/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
index af4dee8654..acb8886a52 100644
--- a/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
+++ b/core/src/test/java/org/apache/gravitino/catalog/TestCatalogManager.java
@@ -19,6 +19,12 @@
 package org.apache.gravitino.catalog;
 
 import static org.apache.gravitino.StringIdentifier.ID_KEY;
+import static org.apache.gravitino.TestCatalog.PROPERTY_KEY1;
+import static org.apache.gravitino.TestCatalog.PROPERTY_KEY2;
+import static org.apache.gravitino.TestCatalog.PROPERTY_KEY3;
+import static org.apache.gravitino.TestCatalog.PROPERTY_KEY4;
+import static org.apache.gravitino.TestCatalog.PROPERTY_KEY5_PREFIX;
+import static org.apache.gravitino.TestCatalog.PROPERTY_KEY6_PREFIX;
 import static org.mockito.ArgumentMatchers.any;
 
 import com.google.common.collect.ImmutableMap;
@@ -28,17 +34,20 @@ import java.io.IOException;
 import java.time.Instant;
 import java.util.Map;
 import java.util.Set;
+import org.apache.commons.lang3.reflect.FieldUtils;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.CatalogChange;
 import org.apache.gravitino.Config;
 import org.apache.gravitino.Configs;
 import org.apache.gravitino.EntityStore;
+import org.apache.gravitino.GravitinoEnv;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.Namespace;
 import org.apache.gravitino.exceptions.CatalogAlreadyExistsException;
 import org.apache.gravitino.exceptions.CatalogInUseException;
 import org.apache.gravitino.exceptions.NoSuchCatalogException;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
+import org.apache.gravitino.lock.LockManager;
 import org.apache.gravitino.meta.AuditInfo;
 import org.apache.gravitino.meta.BaseMetalake;
 import org.apache.gravitino.meta.SchemaEntity;
@@ -77,7 +86,7 @@ public class TestCatalogManager {
           .build();
 
   @BeforeAll
-  public static void setUp() throws IOException {
+  public static void setUp() throws IOException, IllegalAccessException {
     config = new Config(false) {};
     config.set(Configs.CATALOG_LOAD_ISOLATED, false);
 
@@ -87,6 +96,7 @@ public class TestCatalogManager {
     entityStore.put(metalakeEntity, true);
 
     catalogManager = new CatalogManager(config, entityStore, new 
RandomIdGenerator());
+    FieldUtils.writeField(GravitinoEnv.getInstance(), "lockManager", new 
LockManager(config), true);
     catalogManager = Mockito.spy(catalogManager);
   }
 
@@ -123,8 +133,9 @@ public class TestCatalogManager {
     // key1 is required;
     Map<String, String> props1 =
         ImmutableMap.<String, String>builder()
-            .put("key2", "value2")
-            .put("key1", "value1")
+            .put(PROPERTY_KEY2, "value2")
+            .put(PROPERTY_KEY1, "value1")
+            .put(PROPERTY_KEY5_PREFIX + "1", "value1")
             .put("mock", "mock")
             .build();
     Assertions.assertDoesNotThrow(
@@ -136,10 +147,12 @@ public class TestCatalogManager {
     // key1 is required;
     Map<String, String> props2 =
         ImmutableMap.<String, String>builder()
-            .put("key2", "value2")
-            .put("key1", "value1")
-            .put("key3", "3")
-            .put("key4", "value4")
+            .put(PROPERTY_KEY2, "value2")
+            .put(PROPERTY_KEY1, "value1")
+            .put(PROPERTY_KEY3, "3")
+            .put(PROPERTY_KEY4, "value4")
+            .put(PROPERTY_KEY5_PREFIX + "1", "value1")
+            .put(PROPERTY_KEY6_PREFIX + "1", "value1")
             .put("mock", "mock")
             .build();
     Assertions.assertDoesNotThrow(
@@ -147,30 +160,41 @@ public class TestCatalogManager {
             catalogManager.createCatalog(
                 ident2, Catalog.Type.RELATIONAL, provider, "comment", props2));
 
-    CatalogChange change1 = CatalogChange.setProperty("key1", "value1");
+    CatalogChange change1 = CatalogChange.setProperty(PROPERTY_KEY1, "value1");
     Exception e1 =
         Assertions.assertThrows(
             IllegalArgumentException.class, () -> 
catalogManager.alterCatalog(ident, change1));
     Assertions.assertTrue(e1.getMessage().contains("Property key1 is 
immutable"));
 
-    CatalogChange change2 = CatalogChange.setProperty("key3", "value2");
+    CatalogChange change2 = CatalogChange.setProperty(PROPERTY_KEY3, "value2");
     Exception e2 =
         Assertions.assertThrows(
             IllegalArgumentException.class, () -> 
catalogManager.alterCatalog(ident2, change2));
     Assertions.assertTrue(e2.getMessage().contains("Property key3 is 
immutable"));
 
     Assertions.assertDoesNotThrow(
-        () -> catalogManager.alterCatalog(ident2, 
CatalogChange.setProperty("key4", "value4")));
+        () ->
+            catalogManager.alterCatalog(
+                ident2, CatalogChange.setProperty(PROPERTY_KEY4, "value4")));
     Assertions.assertDoesNotThrow(
-        () -> catalogManager.alterCatalog(ident2, 
CatalogChange.setProperty("key2", "value2")));
+        () ->
+            catalogManager.alterCatalog(
+                ident2, CatalogChange.setProperty(PROPERTY_KEY2, "value2")));
 
-    CatalogChange change3 = CatalogChange.setProperty("key4", "value4");
-    CatalogChange change4 = CatalogChange.removeProperty("key1");
+    CatalogChange change3 = CatalogChange.setProperty(PROPERTY_KEY4, "value4");
+    CatalogChange change4 = CatalogChange.removeProperty(PROPERTY_KEY1);
     Exception e3 =
         Assertions.assertThrows(
             IllegalArgumentException.class,
             () -> catalogManager.alterCatalog(ident2, change3, change4));
     Assertions.assertTrue(e3.getMessage().contains("Property key1 is 
immutable"));
+
+    CatalogChange change5 = CatalogChange.setProperty(PROPERTY_KEY6_PREFIX + 
"1", "value1");
+    e3 =
+        Assertions.assertThrows(
+            IllegalArgumentException.class, () -> 
catalogManager.alterCatalog(ident2, change5));
+    Assertions.assertTrue(
+        e3.getMessage().contains("Property key6-1 is immutable"), 
e3.getMessage());
     reset();
   }
 
@@ -186,37 +210,62 @@ public class TestCatalogManager {
 
     // key1 is required;
     Map<String, String> props1 =
-        ImmutableMap.<String, String>builder().put("key2", 
"value2").put("mock", "mock").build();
+        ImmutableMap.<String, String>builder()
+            .put(PROPERTY_KEY2, "value2")
+            .put(PROPERTY_KEY5_PREFIX + "1", "value1")
+            .put("mock", "mock")
+            .build();
     IllegalArgumentException e1 =
         Assertions.assertThrows(
             IllegalArgumentException.class,
             () ->
                 catalogManager.createCatalog(
                     ident, Catalog.Type.RELATIONAL, provider, "comment", 
props1));
-    Assertions.assertTrue(
-        e1.getMessage().contains("Properties are required and must be set: 
[key1]"));
+    Assertions.assertEquals(
+        "Properties or property prefixes are required and must be set: 
[key1]", e1.getMessage());
     // BUG here, in memory does not support rollback
     reset();
 
     // key2 is required;
     Map<String, String> props2 =
-        ImmutableMap.<String, String>builder().put("key1", 
"value1").put("mock", "mock").build();
+        ImmutableMap.<String, String>builder()
+            .put(PROPERTY_KEY1, "value1")
+            .put(PROPERTY_KEY5_PREFIX + "1", "value2")
+            .put("mock", "mock")
+            .build();
     e1 =
         Assertions.assertThrows(
             IllegalArgumentException.class,
             () ->
                 catalogManager.createCatalog(
                     ident, Catalog.Type.RELATIONAL, provider, "comment", 
props2));
-    Assertions.assertTrue(
-        e1.getMessage().contains("Properties are required and must be set: 
[key2]"));
+    Assertions.assertEquals(
+        "Properties or property prefixes are required and must be set: 
[key2]", e1.getMessage());
     reset();
 
+    // property with fixed prefix key5- is required;
+    Map<String, String> props4 =
+        ImmutableMap.<String, String>builder()
+            .put(PROPERTY_KEY1, "value1")
+            .put(PROPERTY_KEY2, "value2")
+            .put("mock", "mock")
+            .build();
+    e1 =
+        Assertions.assertThrows(
+            IllegalArgumentException.class,
+            () ->
+                catalogManager.createCatalog(
+                    ident, Catalog.Type.RELATIONAL, provider, "comment", 
props4));
+    Assertions.assertEquals(
+        "Properties or property prefixes are required and must be set: 
[key5-]", e1.getMessage());
+
     // key3 is optional, but we assign a wrong value format
     Map<String, String> props3 =
         ImmutableMap.<String, String>builder()
-            .put("key1", "value1")
-            .put("key2", "value2")
-            .put("key3", "a12a1a")
+            .put(PROPERTY_KEY1, "value1")
+            .put(PROPERTY_KEY2, "value2")
+            .put(PROPERTY_KEY3, "a12a1a")
+            .put(PROPERTY_KEY5_PREFIX + "1", "value1")
             .put("mock", "mock")
             .build();
     e1 =
@@ -225,7 +274,7 @@ public class TestCatalogManager {
             () ->
                 catalogManager.createCatalog(
                     ident, Catalog.Type.RELATIONAL, provider, "comment", 
props3));
-    Assertions.assertTrue(e1.getMessage().contains("Invalid value: 'a12a1a' 
for property: 'key3'"));
+    Assertions.assertEquals("Invalid value: 'a12a1a' for property: 'key3'", 
e1.getMessage());
     reset();
   }
 
@@ -234,8 +283,9 @@ public class TestCatalogManager {
     NameIdentifier ident = NameIdentifier.of("metalake", "test1");
 
     Map<String, String> props = Maps.newHashMap();
-    props.put("key1", "value1");
-    props.put("key2", "value2");
+    props.put(PROPERTY_KEY1, "value1");
+    props.put(PROPERTY_KEY2, "value2");
+    props.put(PROPERTY_KEY5_PREFIX + "1", "value3");
 
     // test before creation
     Assertions.assertDoesNotThrow(
@@ -301,7 +351,9 @@ public class TestCatalogManager {
                 catalogManager.createCatalog(
                     failedIdent, Catalog.Type.RELATIONAL, provider, "comment", 
props));
     Assertions.assertTrue(
-        exception3.getMessage().contains("Properties are reserved and cannot 
be set"),
+        exception3
+            .getMessage()
+            .contains("Properties or property prefixes are reserved and cannot 
be set"),
         exception3.getMessage());
     
Assertions.assertNull(CatalogManager.catalogCache.getIfPresent(failedIdent));
     // Test failed for the second time
@@ -312,7 +364,9 @@ public class TestCatalogManager {
                 catalogManager.createCatalog(
                     failedIdent, Catalog.Type.RELATIONAL, provider, "comment", 
props));
     Assertions.assertTrue(
-        exception4.getMessage().contains("Properties are reserved and cannot 
be set"),
+        exception4
+            .getMessage()
+            .contains("Properties or property prefixes are reserved and cannot 
be set"),
         exception4.getMessage());
     
Assertions.assertNull(CatalogManager.catalogCache.getIfPresent(failedIdent));
   }
@@ -322,7 +376,15 @@ public class TestCatalogManager {
     NameIdentifier ident = NameIdentifier.of("metalake", "test11");
     NameIdentifier ident1 = NameIdentifier.of("metalake", "test12");
     Map<String, String> props =
-        ImmutableMap.of("provider", "test", "key1", "value1", "key2", 
"value2");
+        ImmutableMap.of(
+            "provider",
+            "test",
+            PROPERTY_KEY1,
+            "value1",
+            PROPERTY_KEY2,
+            "value2",
+            PROPERTY_KEY5_PREFIX + "1",
+            "value3");
 
     catalogManager.createCatalog(ident, Catalog.Type.RELATIONAL, provider, 
"comment", props);
     catalogManager.createCatalog(ident1, Catalog.Type.RELATIONAL, provider, 
"comment", props);
@@ -345,7 +407,15 @@ public class TestCatalogManager {
     NameIdentifier relIdent = NameIdentifier.of("metalake", "catalog_rel");
     NameIdentifier fileIdent = NameIdentifier.of("metalake", "catalog_file");
     Map<String, String> props =
-        ImmutableMap.of("provider", "test", "key1", "value1", "key2", 
"value2");
+        ImmutableMap.of(
+            "provider",
+            "test",
+            PROPERTY_KEY1,
+            "value1",
+            PROPERTY_KEY2,
+            "value2",
+            PROPERTY_KEY5_PREFIX + "1",
+            "value3");
 
     catalogManager.createCatalog(relIdent, Catalog.Type.RELATIONAL, provider, 
"comment", props);
     catalogManager.createCatalog(fileIdent, Catalog.Type.FILESET, provider, 
"comment", props);
@@ -378,7 +448,15 @@ public class TestCatalogManager {
   public void testLoadCatalog() {
     NameIdentifier ident = NameIdentifier.of("metalake", "test21");
     Map<String, String> props =
-        ImmutableMap.of("provider", "test", "key1", "value1", "key2", 
"value2");
+        ImmutableMap.of(
+            "provider",
+            "test",
+            PROPERTY_KEY1,
+            "value1",
+            PROPERTY_KEY2,
+            "value2",
+            PROPERTY_KEY5_PREFIX + "1",
+            "value3");
 
     catalogManager.createCatalog(ident, Catalog.Type.RELATIONAL, provider, 
"comment", props);
 
@@ -404,7 +482,15 @@ public class TestCatalogManager {
   public void testAlterCatalog() {
     NameIdentifier ident = NameIdentifier.of("metalake", "test31");
     Map<String, String> props =
-        ImmutableMap.of("provider", "test", "key1", "value1", "key2", 
"value2");
+        ImmutableMap.of(
+            "provider",
+            "test",
+            PROPERTY_KEY1,
+            "value1",
+            PROPERTY_KEY2,
+            "value2",
+            PROPERTY_KEY5_PREFIX + "1",
+            "value3");
     String comment = "comment";
 
     catalogManager.createCatalog(ident, Catalog.Type.RELATIONAL, provider, 
comment, props);
@@ -451,7 +537,15 @@ public class TestCatalogManager {
   public void testDropCatalog() {
     NameIdentifier ident = NameIdentifier.of("metalake", "test41");
     Map<String, String> props =
-        ImmutableMap.of("provider", "test", "key1", "value1", "key2", 
"value2");
+        ImmutableMap.of(
+            "provider",
+            "test",
+            PROPERTY_KEY1,
+            "value1",
+            PROPERTY_KEY2,
+            "value2",
+            PROPERTY_KEY5_PREFIX + "1",
+            "value3");
     String comment = "comment";
 
     catalogManager.createCatalog(ident, Catalog.Type.RELATIONAL, provider, 
comment, props);
@@ -479,7 +573,15 @@ public class TestCatalogManager {
   public void testForceDropCatalog() throws Exception {
     NameIdentifier ident = NameIdentifier.of("metalake", "test41");
     Map<String, String> props =
-        ImmutableMap.of("provider", "test", "key1", "value1", "key2", 
"value2");
+        ImmutableMap.of(
+            "provider",
+            "test",
+            PROPERTY_KEY1,
+            "value1",
+            PROPERTY_KEY2,
+            "value2",
+            PROPERTY_KEY5_PREFIX + "1",
+            "value3");
     String comment = "comment";
     catalogManager.createCatalog(ident, Catalog.Type.RELATIONAL, provider, 
comment, props);
     SchemaEntity schemaEntity =
@@ -507,7 +609,15 @@ public class TestCatalogManager {
   void testAlterMutableProperties() {
     NameIdentifier ident = NameIdentifier.of("metalake", "test41");
     Map<String, String> props =
-        ImmutableMap.of("provider", "test", "key1", "value1", "key2", 
"value2");
+        ImmutableMap.of(
+            "provider",
+            "test",
+            PROPERTY_KEY1,
+            "value1",
+            PROPERTY_KEY2,
+            "value2",
+            PROPERTY_KEY5_PREFIX + "1",
+            "value3");
     String comment = "comment";
 
     Catalog oldCatalog =
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestCatalogNormalizeDispatcher.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestCatalogNormalizeDispatcher.java
index 66cf9874ca..7904f7494f 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestCatalogNormalizeDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestCatalogNormalizeDispatcher.java
@@ -19,6 +19,9 @@
 package org.apache.gravitino.catalog;
 
 import static org.apache.gravitino.Catalog.Type.RELATIONAL;
+import static org.apache.gravitino.TestCatalog.PROPERTY_KEY1;
+import static org.apache.gravitino.TestCatalog.PROPERTY_KEY2;
+import static org.apache.gravitino.TestCatalog.PROPERTY_KEY5_PREFIX;
 
 import com.google.common.collect.ImmutableMap;
 import java.io.IOException;
@@ -98,7 +101,14 @@ public class TestCatalogNormalizeDispatcher {
     String[] legalNames = {"catalog", "_catalog", "1_catalog", "_", "1"};
     for (String legalName : legalNames) {
       NameIdentifier catalogIdent = NameIdentifier.of(metalake, legalName);
-      Map<String, String> props = ImmutableMap.of("key1", "value1", "key2", 
"value2");
+      Map<String, String> props =
+          ImmutableMap.of(
+              PROPERTY_KEY1,
+              "value1",
+              PROPERTY_KEY2,
+              "value2",
+              PROPERTY_KEY5_PREFIX + "1",
+              "value3");
       Catalog catalog =
           catalogNormalizeDispatcher.createCatalog(catalogIdent, RELATIONAL, 
"test", null, props);
       Assertions.assertEquals(legalName, catalog.name());
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestFilesetOperationDispatcher.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestFilesetOperationDispatcher.java
index b9b80b18c0..9dd411b28b 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestFilesetOperationDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestFilesetOperationDispatcher.java
@@ -83,14 +83,14 @@ public class TestFilesetOperationDispatcher extends 
TestOperationDispatcher {
         () ->
             filesetOperationDispatcher.createFileset(
                 filesetIdent1, "comment", Fileset.Type.MANAGED, "test", 
illegalProps),
-        "Properties are required and must be set");
+        "Properties or property prefixes are required and must be set");
 
     Map<String, String> illegalProps2 = ImmutableMap.of("k1", "v1", ID_KEY, 
"test");
     testPropertyException(
         () ->
             filesetOperationDispatcher.createFileset(
                 filesetIdent1, "comment", Fileset.Type.MANAGED, "test", 
illegalProps2),
-        "Properties are reserved and cannot be set",
+        "Properties or property prefixes are reserved and cannot be set",
         "gravitino.identifier");
   }
 
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestModelOperationDispatcher.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestModelOperationDispatcher.java
index b7aada3eef..dd1a1d48c3 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestModelOperationDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestModelOperationDispatcher.java
@@ -94,7 +94,7 @@ public class TestModelOperationDispatcher extends 
TestOperationDispatcher {
     Map<String, String> illegalProps = ImmutableMap.of("k1", "v1", ID_KEY, 
"test");
     testPropertyException(
         () -> modelOperationDispatcher.registerModel(modelIdent, "comment", 
illegalProps),
-        "Properties are reserved and cannot be set",
+        "Properties or property prefixes are reserved and cannot be set",
         ID_KEY);
   }
 
@@ -190,7 +190,7 @@ public class TestModelOperationDispatcher extends 
TestOperationDispatcher {
         () ->
             modelOperationDispatcher.linkModelVersion(
                 modelIdent, "path", aliases, "comment", illegalProps),
-        "Properties are reserved and cannot be set",
+        "Properties or property prefixes are reserved and cannot be set",
         ID_KEY);
   }
 
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestOperationDispatcher.java 
b/core/src/test/java/org/apache/gravitino/catalog/TestOperationDispatcher.java
index 99a51e10d4..5b83a0e22b 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestOperationDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestOperationDispatcher.java
@@ -21,6 +21,9 @@ package org.apache.gravitino.catalog;
 import static org.apache.gravitino.Configs.TREE_LOCK_CLEAN_INTERVAL;
 import static org.apache.gravitino.Configs.TREE_LOCK_MAX_NODE_IN_MEMORY;
 import static org.apache.gravitino.Configs.TREE_LOCK_MIN_NODE_IN_MEMORY;
+import static org.apache.gravitino.TestCatalog.PROPERTY_KEY1;
+import static org.apache.gravitino.TestCatalog.PROPERTY_KEY2;
+import static org.apache.gravitino.TestCatalog.PROPERTY_KEY5_PREFIX;
 import static 
org.apache.gravitino.TestFilesetPropertiesMetadata.TEST_FILESET_HIDDEN_KEY;
 import static 
org.apache.gravitino.utils.NameIdentifierUtil.getCatalogIdentifier;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -103,7 +106,9 @@ public abstract class TestOperationDispatcher {
     FieldUtils.writeField(GravitinoEnv.getInstance(), "lockManager", new 
LockManager(config), true);
 
     NameIdentifier ident = NameIdentifier.of(metalake, catalog);
-    Map<String, String> props = ImmutableMap.of("key1", "value1", "key2", 
"value2");
+    Map<String, String> props =
+        ImmutableMap.of(
+            PROPERTY_KEY1, "value1", PROPERTY_KEY2, "value2", 
PROPERTY_KEY5_PREFIX + "1", "value3");
     catalogManager.createCatalog(ident, Catalog.Type.RELATIONAL, "test", 
"comment", props);
   }
 
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestSchemaOperationDispatcher.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestSchemaOperationDispatcher.java
index 5f7ef050e2..f57a8c4038 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestSchemaOperationDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestSchemaOperationDispatcher.java
@@ -92,14 +92,14 @@ public class TestSchemaOperationDispatcher extends 
TestOperationDispatcher {
 
     testPropertyException(
         () -> dispatcher.createSchema(schemaIdent, "comment", 
illegalSchemaProperties),
-        "Properties are required and must be set");
+        "Properties or property prefixes are required and must be set");
 
     // Test reserved table properties exception
     illegalSchemaProperties.put(COMMENT_KEY, "table comment");
     illegalSchemaProperties.put(ID_KEY, "gravitino.v1.uidfdsafdsa");
     testPropertyException(
         () -> dispatcher.createSchema(schemaIdent, "comment", 
illegalSchemaProperties),
-        "Properties are reserved and cannot be set",
+        "Properties or property prefixes are reserved and cannot be set",
         "comment",
         "gravitino.identifier");
 
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestTableOperationDispatcher.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestTableOperationDispatcher.java
index cbdbc4848a..d0016776c9 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestTableOperationDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestTableOperationDispatcher.java
@@ -130,7 +130,7 @@ public class TestTableOperationDispatcher extends 
TestOperationDispatcher {
         () ->
             tableOperationDispatcher.createTable(
                 tableIdent1, columns, "comment", illegalTableProperties, new 
Transform[0]),
-        "Properties are required and must be set");
+        "Properties or property prefixes are required and must be set");
 
     // Test reserved table properties exception
     illegalTableProperties.put(COMMENT_KEY, "table comment");
@@ -139,7 +139,7 @@ public class TestTableOperationDispatcher extends 
TestOperationDispatcher {
         () ->
             tableOperationDispatcher.createTable(
                 tableIdent1, columns, "comment", illegalTableProperties, new 
Transform[0]),
-        "Properties are reserved and cannot be set",
+        "Properties or property prefixes are reserved and cannot be set",
         "comment",
         "gravitino.identifier");
 
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestTopicOperationDispatcher.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestTopicOperationDispatcher.java
index 7ee545e8e5..0aea8c6604 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestTopicOperationDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestTopicOperationDispatcher.java
@@ -24,6 +24,8 @@ import static 
org.apache.gravitino.Configs.TREE_LOCK_MIN_NODE_IN_MEMORY;
 import static org.apache.gravitino.Entity.EntityType.SCHEMA;
 import static org.apache.gravitino.StringIdentifier.ID_KEY;
 import static org.apache.gravitino.TestBasePropertiesMetadata.COMMENT_KEY;
+import static org.apache.gravitino.TestCatalog.PROPERTY_KEY1;
+import static org.apache.gravitino.TestCatalog.PROPERTY_KEY5_PREFIX;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.eq;
@@ -96,12 +98,13 @@ public class TestTopicOperationDispatcher extends 
TestOperationDispatcher {
     Map<String, String> illegalProps = ImmutableMap.of("k2", "v2");
     testPropertyException(
         () -> topicOperationDispatcher.createTopic(topicIdent1, "comment", 
null, illegalProps),
-        "Properties are required and must be set");
+        "Properties or property prefixes are required and must be set");
 
-    Map<String, String> illegalProps2 = ImmutableMap.of("k1", "v1", ID_KEY, 
"test");
+    Map<String, String> illegalProps2 =
+        ImmutableMap.of(PROPERTY_KEY1, "v1", PROPERTY_KEY5_PREFIX + "1", 
"value1", ID_KEY, "test");
     testPropertyException(
         () -> topicOperationDispatcher.createTopic(topicIdent1, "comment", 
null, illegalProps2),
-        "Properties are reserved and cannot be set",
+        "Properties or property prefixes are reserved and cannot be set",
         "gravitino.identifier");
   }
 
diff --git a/docs/hadoop-catalog.md b/docs/hadoop-catalog.md
index 3b382235f8..1c255484e5 100644
--- a/docs/hadoop-catalog.md
+++ b/docs/hadoop-catalog.md
@@ -122,22 +122,22 @@ Refer to [Schema 
operation](./manage-fileset-metadata-using-gravitino.md#schema-
 
 ### Fileset properties
 
-| Property name                         | Description                          
                                                                  | Default 
value            | Required | Since Version    |
-|---------------------------------------|--------------------------------------------------------------------------------------------------------|--------------------------|----------|------------------|
-| `authentication.impersonation-enable` | Whether to enable impersonation for 
the Hadoop catalog fileset.                                        | The 
parent(schema) value | No       | 0.6.0-incubating |
-| `authentication.type`                 | The type of authentication for 
Hadoop catalog fileset, currently we only support `kerberos`, `simple`. | The 
parent(schema) value | No       | 0.6.0-incubating |
-| `authentication.kerberos.principal`   | The principal of the Kerberos 
authentication for the fileset.                                          | The 
parent(schema) value | No       | 0.6.0-incubating |
-| `authentication.kerberos.keytab-uri`  | The URI of The keytab for the 
Kerberos authentication for the fileset.                                 | The 
parent(schema) value | No       | 0.6.0-incubating |
-| `credential-providers`                | The credential provider types, 
separated by comma.                                                     | 
(none)                   | No       | 0.8.0-incubating |
-| `placeholder-`                        | Properties that start with 
`placeholder-` are used to replace placeholders in the location.            | 
(none)                   | No       | 0.9.0-incubating |
+| Property name                         | Description                          
                                                                  | Default 
value            | Required | Immutable | Since Version    |
+|---------------------------------------|--------------------------------------------------------------------------------------------------------|--------------------------|----------|-----------|------------------|
+| `authentication.impersonation-enable` | Whether to enable impersonation for 
the Hadoop catalog fileset.                                        | The 
parent(schema) value | No       | Yes       | 0.6.0-incubating |
+| `authentication.type`                 | The type of authentication for 
Hadoop catalog fileset, currently we only support `kerberos`, `simple`. | The 
parent(schema) value | No       | No        | 0.6.0-incubating |
+| `authentication.kerberos.principal`   | The principal of the Kerberos 
authentication for the fileset.                                          | The 
parent(schema) value | No       | No        | 0.6.0-incubating |
+| `authentication.kerberos.keytab-uri`  | The URI of The keytab for the 
Kerberos authentication for the fileset.                                 | The 
parent(schema) value | No       | No        | 0.6.0-incubating |
+| `credential-providers`                | The credential provider types, 
separated by comma.                                                     | 
(none)                   | No       | No        | 0.8.0-incubating |
+| `placeholder-`                        | Properties that start with 
`placeholder-` are used to replace placeholders in the location.            | 
(none)                   | No       | Yes       | 0.9.0-incubating |
 
 Some properties are reserved and cannot be set by users:
 
-| Property name         | Description                           | Default 
value               | Since Version    |
-|-----------------------|---------------------------------------|-----------------------------|------------------|
-| `placeholder-catalog` | The placeholder for the catalog name. | catalog name 
of the fileset | 0.9.0-incubating |
-| `placeholder-schema`  | The placeholder for the schema name.  | schema name 
of the fileset  | 0.9.0-incubating |
-| `placeholder-fileset` | The placeholder for the fileset name. | fileset name 
               | 0.9.0-incubating |
+| Property name         | Description                                          
                                                    | Default value             
  | Since Version    |
+|-----------------------|----------------------------------------------------------------------------------------------------------|-----------------------------|------------------|
+| `placeholder-catalog` | The placeholder for the catalog name.                
                                                    | catalog name of the 
fileset | 0.9.0-incubating |
+| `placeholder-schema`  | The placeholder for the schema name.                 
                                                    | schema name of the 
fileset  | 0.9.0-incubating |
+| `placeholder-fileset` | The placeholder for the fileset name.                
                                                    | fileset name              
  | 0.9.0-incubating |
 
 Credential providers can be specified in several places, as listed below. 
Gravitino checks the `credential-providers` setting in the following order of 
precedence:
 

Reply via email to