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

jerryshao 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 2fe1b28f68 [#14123] feat(fileset): Support managed and external type 
conversion (#14124)
2fe1b28f68 is described below

commit 2fe1b28f68af92d79193a660ef309bcd96d585b7
Author: mchades <[email protected]>
AuthorDate: Fri Oct 23 14:28:04 2026 +0800

    [#14123] feat(fileset): Support managed and external type conversion 
(#14124)
    
    ### What changes were proposed in this pull request?
    
    Add `FilesetChange.updateType`, Python `update_type`, and REST
    `updateType` support for changing existing filesets between `MANAGED`
    and `EXTERNAL`. Conversion preserves the fileset ID, locations, and
    files. Update the OpenAPI specification and usage documentation.
    
    Also correct the quick-start wording that an existing path automatically
    makes a fileset external. This is a documentation correction; creation
    behavior is unchanged.
    
    ### Why are the changes needed?
    
    Users need to change an existing fileset's lifecycle ownership without
    dropping and recreating it. Converting to external allows its
    registration to be dropped while retaining the data; converting to
    managed explicitly accepts deletion on drop.
    
    Related: #12550 and #13506.
    
    Fix: #14123
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. A new fileset alter operation in Java, Python, and REST. Conversion
    does not move, create, or delete files; subsequent direct and
    schema-cascade drops use the updated type.
    
    ### How was this patch tested?
    
    - 280 targeted Java tests passed: `TestFilesetChange`,
    `TestFilesetUpdatesRequest`, `TestFilesetCatalog`,
    `TestFilesetOperations`, and `TestFilesetCatalogOperations` with
    `-PskipITs`.
    - 3 Python tests passed: `python -m unittest
    tests.unittests.test_fileset_type_change
    tests.unittests.test_fileset_catalog_api`.
    - `./gradlew :docs:build`, Spotless, Black, Pylint, and `git diff
    --check` passed.
    - Added coverage for both directions, preserved identity and data,
    multiple locations, repeated conversion, batched changes, direct drop,
    and schema cascade.
    
    Docker integration tests were not run.
    
    ---------
    
    Co-authored-by: Qi Yu <[email protected]>
---
 .../org/apache/gravitino/file/FilesetChange.java   |  61 ++++++++++-
 .../apache/gravitino/file/TestFilesetChange.java   |  18 ++++
 .../catalog/fileset/FilesetCatalogOperations.java  |   5 +-
 .../fileset/TestFilesetCatalogOperations.java      | 118 +++++++++++++++++++++
 .../org/apache/gravitino/client/DTOConverters.java |   3 +
 .../gravitino/client/TestFilesetCatalog.java       |  25 +++++
 .../test/authorization/FilesetAuthorizationIT.java |  50 +++++++++
 .../gravitino/api/file/fileset_change.py           |  51 ++++++++-
 .../gravitino/client/fileset_catalog.py            |   2 +
 .../dto/requests/fileset_update_request.py         |  20 ++++
 .../tests/unittests/test_fileset_type_change.py    |  71 +++++++++++++
 .../dto/requests/FilesetUpdateRequest.java         |  33 ++++++
 .../dto/requests/TestFilesetUpdatesRequest.java    |  45 ++++++++
 docs/filesets.md                                   |  22 ++--
 docs/manage-fileset-metadata-using-gravitino.md    |  53 +++++++++
 docs/open-api/filesets.yaml                        |  28 +++++
 .../server/web/rest/TestFilesetOperations.java     |  33 ++++++
 17 files changed, 626 insertions(+), 12 deletions(-)

diff --git a/api/src/main/java/org/apache/gravitino/file/FilesetChange.java 
b/api/src/main/java/org/apache/gravitino/file/FilesetChange.java
index 5d5e5aecb5..44c13e9369 100644
--- a/api/src/main/java/org/apache/gravitino/file/FilesetChange.java
+++ b/api/src/main/java/org/apache/gravitino/file/FilesetChange.java
@@ -25,7 +25,8 @@ import org.apache.gravitino.secret.SecretReference;
 
 /**
  * A fileset change is a change to a fileset. It can be used to rename a 
fileset, update the comment
- * of a fileset, set a property and value pair for a fileset, or remove a 
property from a fileset.
+ * of a fileset, update the type of a fileset, set a property and value pair 
for a fileset, or
+ * remove a property from a fileset.
  */
 @Evolving
 public interface FilesetChange {
@@ -50,6 +51,22 @@ public interface FilesetChange {
     return new UpdateFilesetComment(newComment);
   }
 
+  /**
+   * Creates a new fileset change to update the fileset type.
+   *
+   * <p>Changing the type preserves the fileset's identity, storage locations, 
and existing files.
+   * Changing to {@link Fileset.Type#EXTERNAL} relinquishes management of the 
data, so dropping the
+   * fileset retains it. Changing to {@link Fileset.Type#MANAGED} accepts 
management of the data,
+   * including deleting it when the fileset is dropped. Setting the current 
type is allowed.
+   *
+   * @param newType The new type of the fileset.
+   * @return The fileset change.
+   * @throws IllegalArgumentException if the new type is null.
+   */
+  static FilesetChange updateType(Fileset.Type newType) {
+    return new UpdateFilesetType(newType);
+  }
+
   /**
    * Creates a new fileset change to set the property and value for the 
fileset.
    *
@@ -215,6 +232,48 @@ public interface FilesetChange {
     }
   }
 
+  /** A fileset change to update the fileset type without changing its data or 
locations. */
+  final class UpdateFilesetType implements FilesetChange {
+    private final Fileset.Type newType;
+
+    private UpdateFilesetType(Fileset.Type newType) {
+      if (newType == null) {
+        throw new IllegalArgumentException("New fileset type cannot be null");
+      }
+      this.newType = newType;
+    }
+
+    /**
+     * Returns the new type of the fileset.
+     *
+     * @return The new fileset type.
+     */
+    public Fileset.Type getNewType() {
+      return newType;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) return true;
+      if (o == null || getClass() != o.getClass()) return false;
+      UpdateFilesetType that = (UpdateFilesetType) o;
+      return newType == that.newType;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+      return Objects.hash(newType);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+      return "UPDATEFILESETTYPE " + newType;
+    }
+  }
+
   /** A fileset change to set the property and value for the fileset. */
   final class SetProperty implements FilesetChange {
     private final String property;
diff --git a/api/src/test/java/org/apache/gravitino/file/TestFilesetChange.java 
b/api/src/test/java/org/apache/gravitino/file/TestFilesetChange.java
index fd8a96ffe1..df17550e6e 100644
--- a/api/src/test/java/org/apache/gravitino/file/TestFilesetChange.java
+++ b/api/src/test/java/org/apache/gravitino/file/TestFilesetChange.java
@@ -23,6 +23,24 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 public class TestFilesetChange {
+  @Test
+  void testUpdateTypeChange() {
+    for (Fileset.Type type : Fileset.Type.values()) {
+      FilesetChange.UpdateFilesetType change =
+          (FilesetChange.UpdateFilesetType) FilesetChange.updateType(type);
+      Assertions.assertEquals(type, change.getNewType());
+      Assertions.assertEquals(change, FilesetChange.updateType(type));
+      Assertions.assertEquals(change.hashCode(), 
FilesetChange.updateType(type).hashCode());
+      Assertions.assertNotEquals(change, null);
+      Assertions.assertNotEquals(change, FilesetChange.rename("fileset"));
+      Assertions.assertEquals("UPDATEFILESETTYPE " + type, change.toString());
+    }
+    Assertions.assertNotEquals(
+        FilesetChange.updateType(Fileset.Type.MANAGED),
+        FilesetChange.updateType(Fileset.Type.EXTERNAL));
+    Assertions.assertThrows(IllegalArgumentException.class, () -> 
FilesetChange.updateType(null));
+  }
+
   @Test
   void testRemovePropertyChange() {
     String property = "property1";
diff --git 
a/catalogs/catalog-fileset/src/main/java/org/apache/gravitino/catalog/fileset/FilesetCatalogOperations.java
 
b/catalogs/catalog-fileset/src/main/java/org/apache/gravitino/catalog/fileset/FilesetCatalogOperations.java
index e00f57a26e..1183684da7 100644
--- 
a/catalogs/catalog-fileset/src/main/java/org/apache/gravitino/catalog/fileset/FilesetCatalogOperations.java
+++ 
b/catalogs/catalog-fileset/src/main/java/org/apache/gravitino/catalog/fileset/FilesetCatalogOperations.java
@@ -1292,6 +1292,7 @@ public class FilesetCatalogOperations extends 
ManagedSchemaOperations
             : Maps.newHashMap(filesetEntity.properties());
     String newName = ident.name();
     String newComment = filesetEntity.comment();
+    Fileset.Type newType = filesetEntity.filesetType();
 
     for (FilesetChange change : changes) {
       if (change instanceof FilesetChange.SetProperty) {
@@ -1304,6 +1305,8 @@ public class FilesetCatalogOperations extends 
ManagedSchemaOperations
         newName = ((FilesetChange.RenameFileset) change).getNewName();
       } else if (change instanceof FilesetChange.UpdateFilesetComment) {
         newComment = ((FilesetChange.UpdateFilesetComment) 
change).getNewComment();
+      } else if (change instanceof FilesetChange.UpdateFilesetType) {
+        newType = ((FilesetChange.UpdateFilesetType) change).getNewType();
       } else if (change instanceof FilesetChange.RemoveComment) {
         newComment = null;
       } else {
@@ -1317,7 +1320,7 @@ public class FilesetCatalogOperations extends 
ManagedSchemaOperations
         .withNamespace(ident.namespace())
         .withId(filesetEntity.id())
         .withComment(newComment)
-        .withFilesetType(filesetEntity.filesetType())
+        .withFilesetType(newType)
         .withStorageLocations(filesetEntity.storageLocations())
         .withProperties(props)
         .withAuditInfo(
diff --git 
a/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/TestFilesetCatalogOperations.java
 
b/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/TestFilesetCatalogOperations.java
index fd46f36805..0c7f1c33fe 100644
--- 
a/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/TestFilesetCatalogOperations.java
+++ 
b/catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/TestFilesetCatalogOperations.java
@@ -163,6 +163,7 @@ import org.junit.jupiter.api.Timeout;
 import org.junit.jupiter.api.io.TempDir;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.CsvSource;
 import org.junit.jupiter.params.provider.EnumSource;
 import org.junit.jupiter.params.provider.MethodSource;
 import org.junit.jupiter.params.provider.ValueSource;
@@ -1398,6 +1399,123 @@ public class TestFilesetCatalogOperations {
     }
   }
 
+  @ParameterizedTest
+  @CsvSource({"EXTERNAL,false", "MANAGED,false", "EXTERNAL,true", 
"MANAGED,true"})
+  void testUpdateFilesetTypePreservesDataAndControlsDrop(
+      Fileset.Type targetType, boolean cascade, @TempDir File root) throws 
IOException {
+    String schemaName = "schema" + generateTestId();
+    createSchema(schemaName, "schema comment", null, null);
+    NameIdentifier ident = NameIdentifier.of("m1", "c1", schemaName, 
"fileset");
+    Fileset.Type originalType =
+        targetType == Fileset.Type.MANAGED ? Fileset.Type.EXTERNAL : 
Fileset.Type.MANAGED;
+    File primary = new File(root, "primary");
+    File backup = new File(root, "backup");
+    Files.createDirectories(primary.toPath());
+    Files.createDirectories(backup.toPath());
+    Map<String, String> locations =
+        ImmutableMap.of("primary", primary.toURI().toString(), "backup", 
backup.toURI().toString());
+    Map<String, String> props =
+        StringIdentifier.newPropertiesWithId(
+            StringIdentifier.fromId(idGenerator.nextId()),
+            ImmutableMap.of(PROPERTY_DEFAULT_LOCATION_NAME, "primary", "k1", 
"v1"));
+
+    try (FilesetCatalogOperations ops = new FilesetCatalogOperations(store, 
secretManager)) {
+      ops.initialize(Maps.newHashMap(), randomCatalogInfo(), 
FILESET_PROPERTIES_METADATA);
+      ops.createMultipleLocationFileset(ident, "comment", originalType, 
locations, props);
+      for (File directory : Arrays.asList(primary, backup)) {
+        Files.writeString(new File(directory, "data.txt").toPath(), "existing 
data");
+      }
+      FilesetEntity before = store.get(ident, Entity.EntityType.FILESET, 
FilesetEntity.class);
+
+      // Repeating the conversion must preserve both metadata and existing 
data.
+      for (int attempt = 0; attempt < 2; attempt++) {
+        Fileset updated = ops.alterFileset(ident, 
FilesetChange.updateType(targetType));
+        Assertions.assertEquals(targetType, updated.type());
+        Assertions.assertEquals(before.name(), updated.name());
+        Assertions.assertEquals(before.comment(), updated.comment());
+        Assertions.assertEquals(before.storageLocations(), 
updated.storageLocations());
+        Assertions.assertEquals(before.properties(), updated.properties());
+        FilesetEntity persisted = store.get(ident, Entity.EntityType.FILESET, 
FilesetEntity.class);
+        Assertions.assertEquals(before.id(), persisted.id());
+        Assertions.assertEquals(before.namespace(), persisted.namespace());
+        Assertions.assertEquals(targetType, persisted.filesetType());
+        Assertions.assertEquals(before.auditInfo().creator(), 
persisted.auditInfo().creator());
+        Assertions.assertEquals(
+            before.auditInfo().createTime(), 
persisted.auditInfo().createTime());
+        Assertions.assertNotNull(persisted.auditInfo().lastModifiedTime());
+        for (File directory : Arrays.asList(primary, backup)) {
+          Assertions.assertEquals(
+              "existing data", Files.readString(new File(directory, 
"data.txt").toPath()));
+        }
+      }
+
+      if (cascade) {
+        Assertions.assertTrue(ops.dropSchema(NameIdentifier.of("m1", "c1", 
schemaName), true));
+      } else {
+        Assertions.assertTrue(ops.dropFileset(ident));
+      }
+      Assertions.assertFalse(store.exists(ident, Entity.EntityType.FILESET));
+      for (File directory : Arrays.asList(primary, backup)) {
+        if (targetType == Fileset.Type.EXTERNAL) {
+          Assertions.assertEquals(
+              "existing data", Files.readString(new File(directory, 
"data.txt").toPath()));
+        } else {
+          Assertions.assertFalse(directory.exists());
+        }
+      }
+    }
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {true, false})
+  void testUpdateFilesetTypeWithOtherChanges(boolean disableFsOps) throws 
IOException {
+    String schemaName = "schema" + generateTestId();
+    createSchema(schemaName, "comment", null, null, true);
+    NameIdentifier ident = NameIdentifier.of("m1", "c1", schemaName, 
"fileset");
+    File missingLocation = new File(UNFORMALIZED_TEST_ROOT_PATH, "missing-" + 
schemaName);
+    createFileset(
+        "fileset",
+        schemaName,
+        "comment",
+        Fileset.Type.EXTERNAL,
+        null,
+        missingLocation.toURI().toString(),
+        true);
+    FilesetEntity before = store.get(ident, Entity.EntityType.FILESET, 
FilesetEntity.class);
+    try (FilesetCatalogOperations ops = new FilesetCatalogOperations(store, 
secretManager)) {
+      ops.initialize(
+          ImmutableMap.of(DISABLE_FILESYSTEM_OPS, 
Boolean.toString(disableFsOps)),
+          randomCatalogInfo(),
+          FILESET_PROPERTIES_METADATA);
+      Fileset updated =
+          ops.alterFileset(
+              ident,
+              FilesetChange.updateType(Fileset.Type.MANAGED),
+              FilesetChange.rename("renamed"),
+              FilesetChange.updateComment("updated"),
+              FilesetChange.setProperty("k1", "v1"));
+      NameIdentifier renamed = NameIdentifier.of(ident.namespace(), "renamed");
+      Assertions.assertEquals(Fileset.Type.MANAGED, updated.type());
+      Assertions.assertEquals("renamed", updated.name());
+      Assertions.assertEquals("updated", updated.comment());
+      Assertions.assertEquals("v1", updated.properties().get("k1"));
+      Assertions.assertFalse(store.exists(ident, Entity.EntityType.FILESET));
+      Assertions.assertEquals(
+          before.id(), store.get(renamed, Entity.EntityType.FILESET, 
FilesetEntity.class).id());
+      Assertions.assertEquals(before.storageLocations(), 
updated.storageLocations());
+      Assertions.assertFalse(missingLocation.exists());
+
+      // Multiple type changes follow request order, like other fileset 
changes.
+      Fileset reverted =
+          ops.alterFileset(
+              renamed,
+              FilesetChange.updateType(Fileset.Type.MANAGED),
+              FilesetChange.updateType(Fileset.Type.EXTERNAL));
+      Assertions.assertEquals(Fileset.Type.EXTERNAL, reverted.type());
+      Assertions.assertTrue(ops.dropFileset(renamed));
+    }
+  }
+
   @Test
   public void testFormalizePath() throws IOException, IllegalAccessException, 
InterruptedException {
 
diff --git 
a/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
 
b/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
index 9bc6fdd0ff..f4ec27b104 100644
--- 
a/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
+++ 
b/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
@@ -286,6 +286,9 @@ class DTOConverters {
     } else if (change instanceof FilesetChange.UpdateFilesetComment) {
       return new FilesetUpdateRequest.UpdateFilesetCommentRequest(
           ((FilesetChange.UpdateFilesetComment) change).getNewComment());
+    } else if (change instanceof FilesetChange.UpdateFilesetType) {
+      return new FilesetUpdateRequest.UpdateFilesetTypeRequest(
+          ((FilesetChange.UpdateFilesetType) change).getNewType());
     } else if (change instanceof FilesetChange.RemoveComment) {
       return new FilesetUpdateRequest.RemoveFilesetCommentRequest();
     } else if (change instanceof FilesetChange.SetProperty) {
diff --git 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestFilesetCatalog.java
 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestFilesetCatalog.java
index f71bdce0e4..7bf900c16c 100644
--- 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestFilesetCatalog.java
+++ 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestFilesetCatalog.java
@@ -62,6 +62,7 @@ import 
org.apache.gravitino.exceptions.NoSuchLocationNameException;
 import org.apache.gravitino.exceptions.NoSuchSchemaException;
 import org.apache.gravitino.exceptions.NotFoundException;
 import org.apache.gravitino.file.Fileset;
+import org.apache.gravitino.file.FilesetChange;
 import org.apache.hc.core5.http.Method;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
@@ -429,6 +430,30 @@ public class TestFilesetCatalog extends TestBase {
         "internal error");
   }
 
+  @Test
+  public void testAlterFilesetType() throws JsonProcessingException {
+    NameIdentifier ident = NameIdentifier.of("schema1", "fileset1");
+    String path =
+        withSlash(
+            FilesetCatalog.formatFilesetRequestPath(
+                    Namespace.of(metalakeName, catalogName, "schema1"))
+                + "/fileset1");
+    for (Fileset.Type type : Fileset.Type.values()) {
+      FilesetDTO expected =
+          mockFilesetDTO("fileset1", type, "comment", "mock location", 
ImmutableMap.of("k1", "v1"));
+      FilesetUpdateRequest request = new 
FilesetUpdateRequest.UpdateFilesetTypeRequest(type);
+      buildMockResource(
+          Method.PUT,
+          path,
+          new FilesetUpdatesRequest(ImmutableList.of(request)),
+          new FilesetResponse(expected),
+          SC_OK);
+      Fileset actual =
+          catalog.asFilesetCatalog().alterFileset(ident, 
FilesetChange.updateType(type));
+      assertFileset(expected, actual);
+    }
+  }
+
   @Test
   public void testGetFileLocation() throws JsonProcessingException {
     NameIdentifier fileset = NameIdentifier.of(metalakeName, catalogName, 
"schema1", "fileset1");
diff --git 
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/FilesetAuthorizationIT.java
 
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/FilesetAuthorizationIT.java
index a651dda514..76a58917cc 100644
--- 
a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/FilesetAuthorizationIT.java
+++ 
b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/FilesetAuthorizationIT.java
@@ -438,6 +438,56 @@ public class FilesetAuthorizationIT extends 
BaseRestApiAuthorizationIT {
     adminFilesetCatalog.dropFileset(filesetIdent);
   }
 
+  /** Verifies that writers can change the fileset lifecycle without gaining 
drop permission. */
+  @Test
+  @Order(9)
+  public void testWriterCanConvertFilesetTypeButCannotDrop() {
+    String filesetName = "type_conversion_fileset";
+    NameIdentifier filesetIdent = NameIdentifier.of(SCHEMA, filesetName);
+    MetadataObject filesetObject =
+        MetadataObjects.of(
+            ImmutableList.of(CATALOG, SCHEMA, filesetName), 
MetadataObject.Type.FILESET);
+    GravitinoMetalake metalake = client.loadMetalake(METALAKE);
+    FilesetCatalog ownerFilesetCatalog = 
metalake.loadCatalog(CATALOG).asFilesetCatalog();
+    FilesetCatalog writerFilesetCatalog =
+        
normalUserClient.loadMetalake(METALAKE).loadCatalog(CATALOG).asFilesetCatalog();
+
+    ownerFilesetCatalog.createFileset(
+        filesetIdent,
+        "comment",
+        Fileset.Type.EXTERNAL,
+        
storageLocation(GravitinoITUtils.genRandomName("FilesetAuthorizationIT_type_conversion")),
+        new HashMap<>());
+    try {
+      metalake.grantPrivilegesToRole(
+          role, filesetObject, 
ImmutableList.of(Privileges.ReadFileset.allow()));
+      assertThrows(
+          ForbiddenException.class,
+          () ->
+              writerFilesetCatalog.alterFileset(
+                  filesetIdent, 
FilesetChange.updateType(Fileset.Type.MANAGED)));
+      assertEquals(Fileset.Type.EXTERNAL, 
ownerFilesetCatalog.loadFileset(filesetIdent).type());
+
+      metalake.grantPrivilegesToRole(
+          role, filesetObject, 
ImmutableList.of(Privileges.WriteFileset.allow()));
+      Fileset managedFileset =
+          writerFilesetCatalog.alterFileset(
+              filesetIdent, FilesetChange.updateType(Fileset.Type.MANAGED));
+      assertEquals(Fileset.Type.MANAGED, managedFileset.type());
+      assertThrows(ForbiddenException.class, () -> 
writerFilesetCatalog.dropFileset(filesetIdent));
+      assertEquals(Fileset.Type.MANAGED, 
writerFilesetCatalog.loadFileset(filesetIdent).type());
+
+      Fileset externalFileset =
+          writerFilesetCatalog.alterFileset(
+              filesetIdent, FilesetChange.updateType(Fileset.Type.EXTERNAL));
+      assertEquals(Fileset.Type.EXTERNAL, externalFileset.type());
+      assertThrows(ForbiddenException.class, () -> 
writerFilesetCatalog.dropFileset(filesetIdent));
+      assertEquals(Fileset.Type.EXTERNAL, 
writerFilesetCatalog.loadFileset(filesetIdent).type());
+    } finally {
+      ownerFilesetCatalog.dropFileset(filesetIdent);
+    }
+  }
+
   private String defaultBaseLocation() {
     if (defaultBaseLocation == null) {
       defaultBaseLocation =
diff --git a/clients/client-python/gravitino/api/file/fileset_change.py 
b/clients/client-python/gravitino/api/file/fileset_change.py
index 7bc2f7bc6d..89c8a947ce 100644
--- a/clients/client-python/gravitino/api/file/fileset_change.py
+++ b/clients/client-python/gravitino/api/file/fileset_change.py
@@ -20,12 +20,14 @@ from dataclasses import dataclass, field
 
 from dataclasses_json import config
 
+from gravitino.api.file.fileset import Fileset
 from gravitino.api.secret import SecretBinding, SecretReference
 
 
 class FilesetChange(ABC):
-    """A fileset change is a change to a fileset. It can be used to rename a 
fileset, update the comment
-    of a fileset, set a property and value pair for a fileset, or remove a 
property from a fileset.
+    """A fileset change is a change to a fileset. It can be used to rename a 
fileset,
+    update the comment of a fileset, update the type of a fileset, set a 
property and
+    value pair for a fileset, or remove a property from a fileset.
     """
 
     @staticmethod
@@ -52,6 +54,25 @@ class FilesetChange(ABC):
         """
         return FilesetChange.UpdateFilesetComment(new_comment)
 
+    @staticmethod
+    def update_type(new_type: Fileset.Type):
+        """Changes the fileset type without changing its identity, locations, 
or files.
+
+        EXTERNAL retains data when the fileset is dropped. MANAGED accepts 
management
+        of the data, including deleting it when the fileset is dropped. 
Setting the
+        current type is allowed.
+
+        Args:
+            new_type: The new fileset type.
+
+        Returns:
+            The fileset change.
+
+        Raises:
+            ValueError: If new_type is not a Fileset.Type.
+        """
+        return FilesetChange.UpdateFilesetType(new_type)
+
     @staticmethod
     def set_property(fileset_property, value):
         """Creates a new fileset change to set the property and value for the 
fileset.
@@ -191,6 +212,32 @@ class FilesetChange(ABC):
             """
             return f"UPDATEFILESETCOMMENT {self._new_comment}"
 
+    @dataclass
+    class UpdateFilesetType:
+        """A fileset change to update the type without changing data or 
locations."""
+
+        _new_type: Fileset.Type = field(metadata=config(field_name="new_type"))
+
+        def __post_init__(self):
+            if not isinstance(self._new_type, Fileset.Type):
+                raise ValueError("New fileset type must be a Fileset.Type")
+
+        def new_type(self) -> Fileset.Type:
+            """Returns the new fileset type."""
+            return self._new_type
+
+        def __eq__(self, other) -> bool:
+            """Compares fileset type changes by their target type."""
+            if not isinstance(other, FilesetChange.UpdateFilesetType):
+                return False
+            return self._new_type == other.new_type()
+
+        def __hash__(self):
+            return hash(self._new_type)
+
+        def __str__(self):
+            return f"UPDATEFILESETTYPE {self._new_type.name}"
+
     @dataclass
     class SetProperty:
         """A fileset change to set the property and value for the fileset."""
diff --git a/clients/client-python/gravitino/client/fileset_catalog.py 
b/clients/client-python/gravitino/client/fileset_catalog.py
index 4d8fed43b9..ef9e0cc8aa 100644
--- a/clients/client-python/gravitino/client/fileset_catalog.py
+++ b/clients/client-python/gravitino/client/fileset_catalog.py
@@ -388,6 +388,8 @@ class FilesetCatalog(
             return FilesetUpdateRequest.UpdateFilesetCommentRequest(
                 change.new_comment()
             )
+        if isinstance(change, FilesetChange.UpdateFilesetType):
+            return 
FilesetUpdateRequest.UpdateFilesetTypeRequest(change.new_type())
         if isinstance(change, FilesetChange.SetProperty):
             return FilesetUpdateRequest.SetFilesetPropertyRequest(
                 change.property(), change.value()
diff --git 
a/clients/client-python/gravitino/dto/requests/fileset_update_request.py 
b/clients/client-python/gravitino/dto/requests/fileset_update_request.py
index 182ab5d46c..204bab6e21 100644
--- a/clients/client-python/gravitino/dto/requests/fileset_update_request.py
+++ b/clients/client-python/gravitino/dto/requests/fileset_update_request.py
@@ -21,6 +21,7 @@ from typing import Dict, Optional
 
 from dataclasses_json import config
 
+from gravitino.api.file.fileset import Fileset
 from gravitino.api.file.fileset_change import FilesetChange
 from gravitino.api.secret import SecretBinding, SecretReference
 from gravitino.rest.rest_message import RESTRequest
@@ -88,6 +89,25 @@ class FilesetUpdateRequest:
             """Returns the fileset change"""
             return FilesetChange.update_comment(self._new_comment)
 
+    @dataclass
+    class UpdateFilesetTypeRequest(FilesetUpdateRequestBase):
+        """Represents a request to change the type of a fileset."""
+
+        _new_type: Fileset.Type = field(metadata=config(field_name="newType"))
+
+        def __init__(self, new_type: Fileset.Type):
+            super().__init__("updateType")
+            self._new_type = new_type
+
+        def validate(self):
+            """Rejects a missing or invalid target type."""
+            if not isinstance(self._new_type, Fileset.Type):
+                raise ValueError('"newType" field must be a Fileset.Type')
+
+        def fileset_change(self):
+            """Returns the fileset type change."""
+            return FilesetChange.update_type(self._new_type)
+
     @dataclass
     class SetFilesetPropertyRequest(FilesetUpdateRequestBase):
         """Represents a request to set a property on a Fileset."""
diff --git a/clients/client-python/tests/unittests/test_fileset_type_change.py 
b/clients/client-python/tests/unittests/test_fileset_type_change.py
new file mode 100644
index 0000000000..17e83a5c57
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_fileset_type_change.py
@@ -0,0 +1,71 @@
+# 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.
+import json
+import unittest
+
+from gravitino.api.file.fileset import Fileset
+from gravitino.api.file.fileset_change import FilesetChange
+from gravitino.client.fileset_catalog import FilesetCatalog
+from gravitino.dto.requests.fileset_update_request import FilesetUpdateRequest
+from gravitino.dto.requests.fileset_updates_request import 
FilesetUpdatesRequest
+
+
+class TestFilesetTypeChange(unittest.TestCase):
+    def test_update_type_request(self):
+        for fileset_type in Fileset.Type:
+            with self.subTest(fileset_type=fileset_type):
+                change = FilesetChange.update_type(fileset_type)
+                self.assertEqual(fileset_type, change.new_type())
+                self.assertEqual(change, 
FilesetChange.update_type(fileset_type))
+                self.assertEqual(
+                    hash(change), hash(FilesetChange.update_type(fileset_type))
+                )
+                self.assertEqual(f"UPDATEFILESETTYPE {fileset_type.name}", 
str(change))
+                request = FilesetCatalog.to_fileset_update_request(change)
+                self.assertEqual(change, request.fileset_change())
+                updates = FilesetUpdatesRequest([request])
+                updates.validate()
+                self.assertEqual(
+                    {
+                        "updates": [
+                            {"@type": "updateType", "newType": 
fileset_type.value}
+                        ]
+                    },
+                    json.loads(updates.to_json()),
+                )
+        self.assertNotEqual(
+            FilesetChange.update_type(Fileset.Type.MANAGED),
+            FilesetChange.update_type(Fileset.Type.EXTERNAL),
+        )
+        self.assertNotEqual(FilesetChange.update_type(Fileset.Type.MANAGED), 
None)
+        self.assertNotEqual(
+            FilesetChange.update_type(Fileset.Type.MANAGED),
+            FilesetChange.rename("fileset"),
+        )
+
+    def test_reject_invalid_type(self):
+        for invalid in (None, "managed", "invalid", 1):
+            with self.subTest(invalid=invalid):
+                with self.assertRaises(ValueError):
+                    FilesetChange.update_type(invalid)
+                with self.assertRaises(ValueError):
+                    FilesetChange.UpdateFilesetType(invalid)
+                request = 
FilesetUpdateRequest.UpdateFilesetTypeRequest(invalid)
+                with self.assertRaises(ValueError):
+                    FilesetUpdatesRequest([request]).validate()
+                with self.assertRaises(ValueError):
+                    request.fileset_change()
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/requests/FilesetUpdateRequest.java
 
b/common/src/main/java/org/apache/gravitino/dto/requests/FilesetUpdateRequest.java
index e811c5ca5b..fe0dd4edd4 100644
--- 
a/common/src/main/java/org/apache/gravitino/dto/requests/FilesetUpdateRequest.java
+++ 
b/common/src/main/java/org/apache/gravitino/dto/requests/FilesetUpdateRequest.java
@@ -31,6 +31,7 @@ import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.ToString;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.file.Fileset;
 import org.apache.gravitino.file.FilesetChange;
 import org.apache.gravitino.rest.RESTRequest;
 import org.apache.gravitino.secret.SecretBinding;
@@ -44,6 +45,9 @@ import org.apache.gravitino.secret.SecretReference;
   @JsonSubTypes.Type(
       value = FilesetUpdateRequest.UpdateFilesetCommentRequest.class,
       name = "updateComment"),
+  @JsonSubTypes.Type(
+      value = FilesetUpdateRequest.UpdateFilesetTypeRequest.class,
+      name = "updateType"),
   @JsonSubTypes.Type(
       value = FilesetUpdateRequest.RemoveFilesetCommentRequest.class,
       name = "removeComment"),
@@ -126,6 +130,35 @@ public interface FilesetUpdateRequest extends RESTRequest {
     public void validate() throws IllegalArgumentException {}
   }
 
+  /** The fileset update request for changing the type of a fileset. */
+  @EqualsAndHashCode
+  @NoArgsConstructor(force = true)
+  @AllArgsConstructor
+  @ToString
+  class UpdateFilesetTypeRequest implements FilesetUpdateRequest {
+
+    @Getter
+    @JsonProperty("newType")
+    private final Fileset.Type newType;
+
+    /** {@inheritDoc} */
+    @Override
+    public FilesetChange filesetChange() {
+      return FilesetChange.updateType(newType);
+    }
+
+    /**
+     * Validates the request.
+     *
+     * @throws IllegalArgumentException if the new type is null.
+     */
+    @Override
+    public void validate() throws IllegalArgumentException {
+      Preconditions.checkArgument(
+          newType != null, "\"newType\" field is required and cannot be null");
+    }
+  }
+
   /** The fileset update request for setting the properties of a fileset. */
   @EqualsAndHashCode
   @NoArgsConstructor(force = true)
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/requests/TestFilesetUpdatesRequest.java
 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestFilesetUpdatesRequest.java
index 92283aceca..2280b8087b 100644
--- 
a/common/src/test/java/org/apache/gravitino/dto/requests/TestFilesetUpdatesRequest.java
+++ 
b/common/src/test/java/org/apache/gravitino/dto/requests/TestFilesetUpdatesRequest.java
@@ -18,12 +18,57 @@
  */
 package org.apache.gravitino.dto.requests;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import java.util.Collections;
+import java.util.Locale;
+import org.apache.gravitino.file.Fileset;
+import org.apache.gravitino.file.FilesetChange;
+import org.apache.gravitino.json.JsonUtils;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 public class TestFilesetUpdatesRequest {
 
+  @Test
+  void testUpdateTypeSerialization() throws JsonProcessingException {
+    for (Fileset.Type type : Fileset.Type.values()) {
+      FilesetUpdateRequest update = new 
FilesetUpdateRequest.UpdateFilesetTypeRequest(type);
+      FilesetUpdatesRequest request = new 
FilesetUpdatesRequest(Collections.singletonList(update));
+      request.validate();
+      String json = JsonUtils.objectMapper().writeValueAsString(request);
+      Assertions.assertEquals(
+          "updateType", 
JsonUtils.objectMapper().readTree(json).at("/updates/0/@type").asText());
+      Assertions.assertEquals(
+          type.name().toLowerCase(Locale.ROOT),
+          
JsonUtils.objectMapper().readTree(json).at("/updates/0/newType").asText());
+      FilesetUpdatesRequest decoded =
+          JsonUtils.objectMapper().readValue(json, 
FilesetUpdatesRequest.class);
+      decoded.validate();
+      Assertions.assertEquals(1, decoded.getUpdates().size());
+      Assertions.assertEquals(
+          FilesetChange.updateType(type), 
decoded.getUpdates().get(0).filesetChange());
+    }
+  }
+
+  @Test
+  void testUpdateTypeValidation() throws JsonProcessingException {
+    for (String update :
+        new String[] {
+          "{\"@type\":\"updateType\"}", 
"{\"@type\":\"updateType\",\"newType\":null}"
+        }) {
+      FilesetUpdateRequest request =
+          JsonUtils.objectMapper().readValue(update, 
FilesetUpdateRequest.class);
+      Assertions.assertThrows(IllegalArgumentException.class, 
request::validate);
+    }
+    Assertions.assertThrows(
+        JsonProcessingException.class,
+        () ->
+            JsonUtils.objectMapper()
+                .readValue(
+                    "{\"@type\":\"updateType\",\"newType\":\"unknown\"}",
+                    FilesetUpdateRequest.class));
+  }
+
   @Test
   public void testValidateWithNullUpdates() {
     // Test that creating a request with null updates throws an exception 
during validation
diff --git a/docs/filesets.md b/docs/filesets.md
index 8dd046f5dc..bc48661371 100644
--- a/docs/filesets.md
+++ b/docs/filesets.md
@@ -44,8 +44,9 @@ than federating an external one, so no provider is needed 
when creating it.
 [Catalogs and Schemas](./catalogs-and-schemas.md). A fileset catalog usually 
carries a base
 `location`, and schemas can narrow it further.
 
-**2. Create the fileset.** Give it a name, a type, and a storage location. 
Creating a managed
-fileset creates the directory; pointing at an existing path makes it external.
+**2. Create the fileset.** Give it a name, a type, and a storage location. 
Choose `MANAGED` to let
+Gravitino manage the data lifecycle, or `EXTERNAL` to retain the data when the 
fileset is dropped.
+Pointing at an existing path does not automatically make a fileset external.
 
 **3. Read it by name.** Engines reach the files through GVFS or the client 
rather than the raw path.
 See [How to use GVFS](./how-to-use-gvfs.md).
@@ -54,14 +55,19 @@ See [How to use GVFS](./how-to-use-gvfs.md).
 
 ### Managed and External
 
-A managed fileset belongs to Gravitino. Creating it creates the directory, and 
deleting it deletes
-the data.
+A managed fileset belongs to Gravitino. Creating it creates the directory if 
needed, and deleting
+it deletes the data.
 
-An external fileset points at a location that already exists and stays under 
someone else's control.
-Deleting it removes the Gravitino record and leaves the files alone.
+An external fileset leaves data lifecycle management to the user. Deleting it 
removes only the
+Gravitino record and preserves the files. With filesystem operations enabled, 
creating either
+type creates the storage directory if it does not exist.
 
-The distinction only matters at deletion time, and it is the single most 
important thing to get
-right when creating one.
+An existing fileset can be converted between the two types using
+[`FilesetChange.updateType`](./manage-fileset-metadata-using-gravitino.md#change-the-fileset-type).
+Conversion changes metadata only and preserves its identity, locations, and 
files. Converting to
+external relinquishes management of the data; converting to managed accepts 
deletion of the data
+when the fileset is dropped. With `disable-filesystem-ops=true`, dropping a 
fileset removes only
+its metadata, even after conversion to `MANAGED`.
 
 ### Storage Locations
 
diff --git a/docs/manage-fileset-metadata-using-gravitino.md 
b/docs/manage-fileset-metadata-using-gravitino.md
index 4b725c74ba..10ab499625 100755
--- a/docs/manage-fileset-metadata-using-gravitino.md
+++ b/docs/manage-fileset-metadata-using-gravitino.md
@@ -156,6 +156,7 @@ Changes are applied as a list in one request. Storage 
locations cannot be change
 
|--------------------|--------------------------------------------------------------|-----------------------------------------------|
 | Rename             | `{"@type":"rename","newName":"fileset_renamed"}`        
     | `FilesetChange.rename("fileset_renamed")`     |
 | Update the comment | `{"@type":"updateComment","newComment":"new_comment"}`  
     | `FilesetChange.updateComment("new_comment")`  |
+| Update the type    | `{"@type":"updateType","newType":"external"}`           
     | `FilesetChange.updateType(Fileset.Type.EXTERNAL)` |
 | Set a property     | 
`{"@type":"setProperty","property":"key1","value":"value1"}` | 
`FilesetChange.setProperty("key1", "value1")` |
 | Remove a property  | `{"@type":"removeProperty","property":"key1"}`          
     | `FilesetChange.removeProperty("key1")`        |
 
@@ -183,6 +184,58 @@ Fileset fileset = filesets.alterFileset(
 </TabItem>
 </Tabs>
 
+#### Change the Fileset Type
+
+An existing fileset can be changed between `MANAGED` and `EXTERNAL`. This 
changes metadata only:
+its identifier, name, storage locations, properties, and existing files are 
preserved. Setting the
+current type is allowed.
+
+Change a `MANAGED` fileset to `EXTERNAL` before dropping it to retain its 
files. Changing an
+`EXTERNAL` fileset to `MANAGED` explicitly accepts management of the existing 
data, including
+deleting that data when the fileset is dropped. The same type semantics apply 
when dropping a
+schema with cascade. Conversion does not move, create, or delete files, and 
does not protect data
+from deletion through another managed fileset with an overlapping location.
+
+With `disable-filesystem-ops=true`, dropping a fileset removes only its 
metadata, even after
+conversion to `MANAGED`.
+
+Type conversion follows the existing [fileset alter 
authorization](security/access-control.md#data-objects).
+A caller with `WRITE_FILESET` and the required catalog and schema use 
privileges can convert in
+either direction, including changing whether a subsequent drop deletes the 
data. This does not
+grant permission to drop the fileset; drop retains its existing ownership 
requirements.
+
+<Tabs groupId='language' queryString>
+<TabItem value="shell" label="REST">
+
+```shell
+curl -X PUT -H "Accept: application/vnd.gravitino.v1+json" \
+  -H "Content-Type: application/json" -d '{
+  "updates": [{"@type": "updateType", "newType": "external"}]
+}' 
http://localhost:8090/api/metalakes/example/catalogs/landing/schemas/raw/filesets/raw_events
+```
+
+</TabItem>
+<TabItem value="java" label="Java">
+
+```java
+Fileset fileset = filesets.alterFileset(
+    NameIdentifier.of("raw", "raw_events"),
+    FilesetChange.updateType(Fileset.Type.EXTERNAL));
+```
+
+</TabItem>
+<TabItem value="python" label="Python">
+
+```python
+fileset = filesets.alter_fileset(
+    NameIdentifier.of("raw", "raw_events"),
+    FilesetChange.update_type(Fileset.Type.EXTERNAL),
+)
+```
+
+</TabItem>
+</Tabs>
+
 ### Drop a Fileset
 
 Dropping a `MANAGED` fileset deletes its files. Dropping an `EXTERNAL` one 
removes only the
diff --git a/docs/open-api/filesets.yaml b/docs/open-api/filesets.yaml
index 39f89cdd57..673e1047f0 100644
--- a/docs/open-api/filesets.yaml
+++ b/docs/open-api/filesets.yaml
@@ -393,6 +393,7 @@ components:
         - $ref: "#/components/schemas/RenameFilesetRequest"
         - $ref: "#/components/schemas/SetFilesetPropertyRequest"
         - $ref: "#/components/schemas/UpdateFilesetCommentRequest"
+        - $ref: "#/components/schemas/UpdateFilesetTypeRequest"
         - $ref: "#/components/schemas/RemoveFilesetPropertyRequest"
         - $ref: "#/components/schemas/SetFilesetSecretBindingRequest"
         - $ref: "#/components/schemas/SetFilesetSecretReferenceRequest"
@@ -402,6 +403,7 @@ components:
           rename: "#/components/schemas/RenameFilesetRequest"
           setProperty: "#/components/schemas/SetFilesetPropertyRequest"
           updateComment: "#/components/schemas/UpdateFilesetCommentRequest"
+          updateType: "#/components/schemas/UpdateFilesetTypeRequest"
           removeProperty: "#/components/schemas/RemoveFilesetPropertyRequest"
           setSecretBinding: 
"#/components/schemas/SetFilesetSecretBindingRequest"
           setSecretReference: 
"#/components/schemas/SetFilesetSecretReferenceRequest"
@@ -468,6 +470,32 @@ components:
         "newComment": "new comment"
       }
 
+    UpdateFilesetTypeRequest:
+      type: object
+      description: >-
+        Changes the fileset type without changing its identity, storage 
locations, or files.
+        Changing to external retains data on drop. Changing to managed accepts 
management
+        of the data, including deleting it on drop. Setting the current type 
is allowed.
+      required:
+        - "@type"
+        - newType
+      properties:
+        "@type":
+          type: string
+          description: The type of the update
+          enum:
+            - updateType
+        newType:
+          type: string
+          description: The new type of the fileset
+          enum:
+            - managed
+            - external
+      example: {
+        "@type": "updateType",
+        "newType": "external"
+      }
+
     RemoveFilesetPropertyRequest:
       type: object
       required:
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestFilesetOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestFilesetOperations.java
index 9b99a7efae..2b541e83d4 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestFilesetOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestFilesetOperations.java
@@ -83,7 +83,9 @@ import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.EnumSource;
 import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.mockito.Mockito;
 import org.mockito.stubbing.Answer;
 
@@ -518,6 +520,37 @@ public class TestFilesetOperations extends 
BaseOperationsTest {
     assertUpdateFileset(new FilesetUpdatesRequest(ImmutableList.of(req)), 
fileset);
   }
 
+  @ParameterizedTest
+  @EnumSource(Fileset.Type.class)
+  void testUpdateFilesetType(Fileset.Type type) {
+    FilesetUpdateRequest req = new 
FilesetUpdateRequest.UpdateFilesetTypeRequest(type);
+    Fileset fileset =
+        mockFileset("fileset1", type, "comment", "mock location", 
ImmutableMap.of("k1", "v1"));
+    assertUpdateFileset(new FilesetUpdatesRequest(ImmutableList.of(req)), 
fileset);
+    Mockito.verify(dispatcher)
+        .alterFileset(
+            NameIdentifier.of(metalake, catalog, schema, "fileset1"),
+            FilesetChange.updateType(type));
+  }
+
+  @ParameterizedTest
+  @ValueSource(
+      strings = {
+        "{\"updates\":[{\"@type\":\"updateType\"}]}",
+        "{\"updates\":[{\"@type\":\"updateType\",\"newType\":null}]}",
+        "{\"updates\":[{\"@type\":\"updateType\",\"newType\":\"unknown\"}]}"
+      })
+  void testUpdateFilesetTypeRejectsInvalidRequest(String json) {
+    try (Response response =
+        target(filesetPath(metalake, catalog, schema) + "fileset1")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .put(Entity.entity(json, MediaType.APPLICATION_JSON_TYPE))) {
+      Assertions.assertEquals(400, response.getStatus());
+      Mockito.verifyNoInteractions(dispatcher);
+    }
+  }
+
   @Test
   public void testRemoveFilesetComment() {
     FilesetUpdateRequest req = new 
FilesetUpdateRequest.UpdateFilesetCommentRequest(null);

Reply via email to