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

mchades 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 72454e3733 [#12605] feat(core): Add Semantic Model list, alter, and 
drop operations (#12833)
72454e3733 is described below

commit 72454e37336b2300acb73085df24d4ccb334fa14
Author: mchades <[email protected]>
AuthorDate: Mon Sep 28 20:29:08 2026 +0800

    [#12605] feat(core): Add Semantic Model list, alter, and drop operations 
(#12833)
    
    ### What changes were proposed in this pull request?
    
    Add managed Core operations for listing, altering, and dropping Semantic
    Models:
    
    - Apply ordered Semantic Model changes atomically.
    - Preserve audit metadata during alterations.
    - Perform complete Core definition validation when replacing the
    definition.
    - Avoid resolving external sources for metadata-only changes.
    - Add dispatcher integration and focused unit and JDBC tests.
    
    ### Why are the changes needed?
    
    Gravitino Core needs managed lifecycle operations for Semantic Models
    after create and load support.
    
    Fix: #12605
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. It implements the existing Semantic Model list, alter, and drop
    operations in Gravitino Core. No new REST, OpenAPI, or client surface is
    introduced.
    
    ### How was this patch tested?
    
    - Focused Core and JDBC tests with Docker tests enabled — 31 tests, 0
    failures, 0 errors
    - `./gradlew :core:check -PskipITs` — 2,448 tests, 0 failures, 0 errors,
    2 skipped
    - `git diff --check origin/main..HEAD`
---
 .../catalog/ManagedSemanticModelOperations.java    | 119 +++++++-
 .../catalog/SemanticModelOperationDispatcher.java  |   3 +
 .../TestManagedSemanticModelOperations.java        | 315 ++++++++++++++++++++-
 .../TestManagedSemanticModelOperationsJDBC.java    | 115 ++++++--
 .../TestSemanticModelOperationDispatcher.java      |  82 +++++-
 5 files changed, 596 insertions(+), 38 deletions(-)

diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/ManagedSemanticModelOperations.java
 
b/core/src/main/java/org/apache/gravitino/catalog/ManagedSemanticModelOperations.java
index 4fc4491797..cc331d737c 100644
--- 
a/core/src/main/java/org/apache/gravitino/catalog/ManagedSemanticModelOperations.java
+++ 
b/core/src/main/java/org/apache/gravitino/catalog/ManagedSemanticModelOperations.java
@@ -21,6 +21,8 @@ package org.apache.gravitino.catalog;
 import com.google.common.base.Preconditions;
 import java.io.IOException;
 import java.time.Instant;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.function.BiConsumer;
 import javax.annotation.Nullable;
@@ -71,9 +73,17 @@ public class ManagedSemanticModelOperations implements 
SemanticModelCatalog {
 
   @Override
   public NameIdentifier[] listSemanticModels(Namespace namespace) throws 
NoSuchSchemaException {
-    // TODO: Implement in the Semantic Model list/alter/drop capability.
-    throw new UnsupportedOperationException(
-        "listSemanticModels: list/alter/drop capability is not implemented");
+    try {
+      List<SemanticModelEntity> models =
+          store.list(namespace, SemanticModelEntity.class, 
Entity.EntityType.SEMANTIC_MODEL);
+      return models.stream()
+          .map(model -> NameIdentifier.of(namespace, model.name()))
+          .toArray(NameIdentifier[]::new);
+    } catch (NoSuchEntityException e) {
+      throw new NoSuchSchemaException(e, "Schema %s does not exist", 
namespace);
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to list Semantic Models in namespace 
" + namespace, e);
+    }
   }
 
   @Override
@@ -131,15 +141,106 @@ public class ManagedSemanticModelOperations implements 
SemanticModelCatalog {
   public SemanticModel alterSemanticModel(NameIdentifier ident, 
SemanticModelChange... changes)
       throws NoSuchSemanticModelException, SemanticModelAlreadyExistsException,
           IllegalSemanticModelException {
-    // TODO: Implement in the Semantic Model list/alter/drop capability.
-    throw new UnsupportedOperationException(
-        "alterSemanticModel: list/alter/drop capability is not implemented");
+    boolean validateForWrite = requiresWriteValidation(changes);
+
+    try {
+      return store.update(
+          ident,
+          SemanticModelEntity.class,
+          Entity.EntityType.SEMANTIC_MODEL,
+          oldEntity -> {
+            SemanticModelEntity candidate = applyChanges(oldEntity, changes);
+            if (validateForWrite) {
+              writeValidator.accept(
+                  NameIdentifier.of(candidate.namespace(), candidate.name()),
+                  candidate.definition());
+            }
+            return candidate;
+          });
+    } catch (NoSuchEntityException e) {
+      throw new NoSuchSemanticModelException(e, "Semantic Model %s does not 
exist", ident);
+    } catch (EntityAlreadyExistsException e) {
+      throw new SemanticModelAlreadyExistsException(
+          e, "A Semantic Model with the requested name already exists while 
altering %s", ident);
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to alter Semantic Model " + ident, e);
+    }
   }
 
   @Override
   public boolean dropSemanticModel(NameIdentifier ident) {
-    // TODO: Implement in the Semantic Model list/alter/drop capability.
-    throw new UnsupportedOperationException(
-        "dropSemanticModel: list/alter/drop capability is not implemented");
+    try {
+      return store.delete(ident, Entity.EntityType.SEMANTIC_MODEL);
+    } catch (NoSuchEntityException e) {
+      return false;
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to drop Semantic Model " + ident, e);
+    }
+  }
+
+  private boolean requiresWriteValidation(SemanticModelChange[] changes) {
+    if (changes == null || changes.length == 0) {
+      throw new IllegalSemanticModelException("At least one Semantic Model 
change is required");
+    }
+
+    boolean requiresWriteValidation = false;
+    for (SemanticModelChange change : changes) {
+      if (change == null) {
+        throw new IllegalSemanticModelException("Semantic Model change must 
not be null");
+      }
+
+      if (change instanceof SemanticModelChange.ReplaceDefinition) {
+        requiresWriteValidation = true;
+      } else if (change instanceof SemanticModelChange.RenameSemanticModel
+          || change instanceof SemanticModelChange.UpdateComment
+          || change instanceof SemanticModelChange.SetProperty
+          || change instanceof SemanticModelChange.RemoveProperty) {
+        // These changes do not modify the definition and must not revisit its 
sources.
+      } else {
+        throw new IllegalSemanticModelException(
+            "Unsupported Semantic Model change: %s", 
change.getClass().getName());
+      }
+    }
+    return requiresWriteValidation;
+  }
+
+  private static SemanticModelEntity applyChanges(
+      SemanticModelEntity oldEntity, SemanticModelChange[] changes) {
+    String newName = oldEntity.name();
+    String newComment = oldEntity.comment();
+    Map<String, String> newProperties = new HashMap<>(oldEntity.properties());
+    SemanticModelDefinition newDefinition = oldEntity.definition();
+
+    for (SemanticModelChange change : changes) {
+      if (change instanceof SemanticModelChange.RenameSemanticModel) {
+        newName = ((SemanticModelChange.RenameSemanticModel) 
change).getNewName();
+      } else if (change instanceof SemanticModelChange.UpdateComment) {
+        newComment = ((SemanticModelChange.UpdateComment) 
change).getNewComment();
+      } else if (change instanceof SemanticModelChange.SetProperty) {
+        SemanticModelChange.SetProperty setProperty = 
(SemanticModelChange.SetProperty) change;
+        newProperties.put(setProperty.getProperty(), setProperty.getValue());
+      } else if (change instanceof SemanticModelChange.RemoveProperty) {
+        newProperties.remove(((SemanticModelChange.RemoveProperty) 
change).getProperty());
+      } else if (change instanceof SemanticModelChange.ReplaceDefinition) {
+        newDefinition = ((SemanticModelChange.ReplaceDefinition) 
change).getDefinition();
+      }
+    }
+
+    AuditInfo oldAudit = oldEntity.auditInfo();
+    return SemanticModelEntity.builder()
+        .withId(oldEntity.id())
+        .withName(newName)
+        .withNamespace(oldEntity.namespace())
+        .withComment(newComment)
+        .withDefinition(newDefinition)
+        .withProperties(newProperties)
+        .withAuditInfo(
+            AuditInfo.builder()
+                .withCreator(oldAudit.creator())
+                .withCreateTime(oldAudit.createTime())
+                .withLastModifier(PrincipalUtils.getCurrentUserName())
+                .withLastModifiedTime(Instant.now())
+                .build())
+        .build();
   }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/SemanticModelOperationDispatcher.java
 
b/core/src/main/java/org/apache/gravitino/catalog/SemanticModelOperationDispatcher.java
index 1be27b1100..4608645a18 100644
--- 
a/core/src/main/java/org/apache/gravitino/catalog/SemanticModelOperationDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/catalog/SemanticModelOperationDispatcher.java
@@ -104,6 +104,9 @@ public class SemanticModelOperationDispatcher extends 
OperationDispatcher
   public SemanticModel alterSemanticModel(NameIdentifier ident, 
SemanticModelChange... changes)
       throws NoSuchSemanticModelException, SemanticModelAlreadyExistsException,
           IllegalSemanticModelException {
+    if (changes == null || changes.length == 0) {
+      throw new IllegalSemanticModelException("At least one Semantic Model 
change is required");
+    }
     checkRelationalCatalog(ident.namespace());
     NameIdentifier schemaIdent = schemaIdentifier(ident);
     if (!schemaDispatcher.schemaExists(schemaIdent)) {
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestManagedSemanticModelOperations.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestManagedSemanticModelOperations.java
index acabf3ec38..0f2745bd03 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestManagedSemanticModelOperations.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestManagedSemanticModelOperations.java
@@ -19,10 +19,13 @@
 package org.apache.gravitino.catalog;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doThrow;
@@ -33,6 +36,9 @@ import static org.mockito.Mockito.when;
 
 import java.io.IOException;
 import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.BiConsumer;
 import org.apache.gravitino.Entity;
 import org.apache.gravitino.EntityAlreadyExistsException;
@@ -172,19 +178,303 @@ public class TestManagedSemanticModelOperations {
   }
 
   @Test
-  public void testRemainingCapabilitiesAreExplicitlyUnsupported() {
+  public void testListAlterAndDropFromMemoryStore() {
+    AtomicInteger writeValidationCount = new AtomicInteger();
+    InMemoryEntityStore store = new InMemoryEntityStore();
+    ManagedSemanticModelOperations operations =
+        new ManagedSemanticModelOperations(
+            store,
+            new RandomIdGenerator(),
+            (ident, definition) -> writeValidationCount.incrementAndGet());
+    SemanticModel first =
+        operations.createSemanticModel(
+            IDENT, "Original", definition("orders"), Map.of("domain", 
"sales"));
+    NameIdentifier secondIdent = NameIdentifier.of(NAMESPACE, 
"inventory_model");
+    operations.createSemanticModel(secondIdent, null, definition("inventory"), 
Map.of());
+
+    NameIdentifier renamedIdent = NameIdentifier.of(NAMESPACE, 
"renamed_sales_model");
+    SemanticModelDefinition replacement = definition("invoices");
+    SemanticModel altered =
+        operations.alterSemanticModel(
+            IDENT,
+            SemanticModelChange.rename(renamedIdent.name()),
+            SemanticModelChange.updateComment("Updated"),
+            SemanticModelChange.setProperty("tier", "gold"),
+            SemanticModelChange.removeProperty("domain"),
+            SemanticModelChange.replaceDefinition(replacement));
+
+    assertEquals(renamedIdent.name(), altered.name());
+    assertEquals("Updated", altered.comment());
+    assertEquals(replacement, altered.definition());
+    assertEquals(Map.of("tier", "gold"), altered.properties());
+    assertEquals(first.auditInfo().creator(), altered.auditInfo().creator());
+    assertEquals(first.auditInfo().createTime(), 
altered.auditInfo().createTime());
+    assertEquals(first.auditInfo().creator(), 
altered.auditInfo().lastModifier());
+    assertNotNull(altered.auditInfo().lastModifiedTime());
+    assertEquals(3, writeValidationCount.get());
+    assertThrows(NoSuchSemanticModelException.class, () -> 
operations.loadSemanticModel(IDENT));
+    assertSame(altered, operations.loadSemanticModel(renamedIdent));
+    assertEquals(
+        Set.of(renamedIdent, secondIdent), 
Set.of(operations.listSemanticModels(NAMESPACE)));
+
+    assertTrue(operations.dropSemanticModel(renamedIdent));
+    assertFalse(operations.dropSemanticModel(renamedIdent));
+    assertEquals(Set.of(secondIdent), 
Set.of(operations.listSemanticModels(NAMESPACE)));
+  }
+
+  @Test
+  public void testSelectiveValidationAndAtomicFailures() {
+    AtomicInteger writeValidationCount = new AtomicInteger();
+    IllegalSemanticModelException sourceFailure =
+        new IllegalSemanticModelException("Replacement source is unavailable");
     ManagedSemanticModelOperations operations =
         new ManagedSemanticModelOperations(
-            mock(EntityStore.class), mock(IdGenerator.class), (ident, 
definition) -> {});
+            new InMemoryEntityStore(),
+            new RandomIdGenerator(),
+            (ident, definition) -> {
+              SemanticModelValidator.validateDefinition(definition);
+              writeValidationCount.incrementAndGet();
+              if (definition.datasets()[0].name().equals("blocked")) {
+                throw sourceFailure;
+              }
+            });
+    operations.createSemanticModel(IDENT, "Original", definition("orders"), 
Map.of());
+
+    operations.alterSemanticModel(IDENT, 
SemanticModelChange.setProperty("owner", "analytics"));
+    operations.alterSemanticModel(IDENT, 
SemanticModelChange.updateComment("Comment only"));
+    assertEquals(1, writeValidationCount.get());
 
+    SemanticModelDefinition accepted = definition("accepted");
+    operations.alterSemanticModel(IDENT, 
SemanticModelChange.replaceDefinition(accepted));
+    assertEquals(2, writeValidationCount.get());
+
+    SemanticModelDefinition duplicateDatasets =
+        SemanticModelDefinition.builder()
+            .withDatasets(new Dataset[] {dataset("duplicate"), 
dataset("duplicate")})
+            .build();
     assertThrows(
-        UnsupportedOperationException.class, () -> 
operations.listSemanticModels(NAMESPACE));
+        IllegalSemanticModelException.class,
+        () ->
+            operations.alterSemanticModel(
+                IDENT, 
SemanticModelChange.replaceDefinition(duplicateDatasets)));
+    assertEquals(2, writeValidationCount.get());
+    assertEquals(accepted, operations.loadSemanticModel(IDENT).definition());
+
+    assertSame(
+        sourceFailure,
+        assertThrows(
+            IllegalSemanticModelException.class,
+            () ->
+                operations.alterSemanticModel(
+                    IDENT, 
SemanticModelChange.replaceDefinition(definition("blocked")))));
+    assertEquals(3, writeValidationCount.get());
+    assertEquals(accepted, operations.loadSemanticModel(IDENT).definition());
+  }
+
+  @Test
+  public void testIndividualMetadataAndPropertyChangesPreserveCreationAudit() {
+    AtomicInteger writeValidationCount = new AtomicInteger();
+    InMemoryEntityStore store = new InMemoryEntityStore();
+    ManagedSemanticModelOperations operations =
+        new ManagedSemanticModelOperations(
+            store,
+            new RandomIdGenerator(),
+            (ident, definition) -> writeValidationCount.incrementAndGet());
+    SemanticModel original =
+        operations.createSemanticModel(
+            IDENT,
+            "Initial comment",
+            definition("orders"),
+            Map.of("overwrite", "old", "remove", "present"));
+
+    SemanticModel propertyUpdated =
+        operations.alterSemanticModel(IDENT, 
SemanticModelChange.setProperty("overwrite", "new"));
+    assertEquals(Map.of("overwrite", "new", "remove", "present"), 
propertyUpdated.properties());
+    assertCreationAuditPreserved(original, propertyUpdated);
+
+    SemanticModel propertiesRemoved =
+        operations.alterSemanticModel(
+            IDENT,
+            SemanticModelChange.removeProperty("remove"),
+            SemanticModelChange.removeProperty("absent"));
+    assertEquals(Map.of("overwrite", "new"), propertiesRemoved.properties());
+    assertCreationAuditPreserved(original, propertiesRemoved);
+
+    SemanticModel commentRemoved =
+        operations.alterSemanticModel(IDENT, 
SemanticModelChange.updateComment(null));
+    assertNull(commentRemoved.comment());
+    assertCreationAuditPreserved(original, commentRemoved);
+
+    NameIdentifier renamedIdent = NameIdentifier.of(NAMESPACE, 
"individually_renamed");
+    SemanticModel renamed =
+        operations.alterSemanticModel(IDENT, 
SemanticModelChange.rename(renamedIdent.name()));
+    assertEquals(renamedIdent.name(), renamed.name());
+    assertEquals(((SemanticModelEntity) original).id(), ((SemanticModelEntity) 
renamed).id());
+    assertCreationAuditPreserved(original, renamed);
+    assertThrows(NoSuchSemanticModelException.class, () -> 
operations.loadSemanticModel(IDENT));
+    assertSame(renamed, operations.loadSemanticModel(renamedIdent));
+    assertEquals(1, writeValidationCount.get());
+  }
+
+  @Test
+  public void 
testLaterMetadataChangesDoNotSkipDefinitionReplacementValidation() {
+    AtomicInteger writeValidationCount = new AtomicInteger();
+    AtomicReference<NameIdentifier> validatedIdent = new AtomicReference<>();
+    AtomicReference<SemanticModelDefinition> validatedDefinition = new 
AtomicReference<>();
+    ManagedSemanticModelOperations operations =
+        new ManagedSemanticModelOperations(
+            new InMemoryEntityStore(),
+            new RandomIdGenerator(),
+            (ident, definition) -> {
+              writeValidationCount.incrementAndGet();
+              validatedIdent.set(ident);
+              validatedDefinition.set(definition);
+            });
+    operations.createSemanticModel(IDENT, "Original", definition("orders"), 
Map.of());
+    SemanticModelDefinition replacement = definition("invoices");
+    NameIdentifier renamedIdent = NameIdentifier.of(NAMESPACE, 
"validated_after_replace");
+
+    SemanticModel altered =
+        operations.alterSemanticModel(
+            IDENT,
+            SemanticModelChange.replaceDefinition(replacement),
+            SemanticModelChange.setProperty("owner", "analytics"),
+            SemanticModelChange.updateComment("Updated after replacement"),
+            SemanticModelChange.rename(renamedIdent.name()));
+
+    assertEquals(2, writeValidationCount.get());
+    assertEquals(renamedIdent, validatedIdent.get());
+    assertSame(replacement, validatedDefinition.get());
+    assertEquals(replacement, altered.definition());
+    assertEquals("Updated after replacement", altered.comment());
+    assertEquals(Map.of("owner", "analytics"), altered.properties());
+  }
+
+  @Test
+  public void testNullInMixedChangeBatchIsRejectedBeforeMutation() {
+    InMemoryEntityStore store = new InMemoryEntityStore();
+    ManagedSemanticModelOperations operations =
+        new ManagedSemanticModelOperations(
+            store, new RandomIdGenerator(), (ident, definition) -> {});
+    SemanticModel original =
+        operations.createSemanticModel(IDENT, null, definition("orders"), 
Map.of("key", "old"));
+
     assertThrows(
-        UnsupportedOperationException.class,
+        IllegalSemanticModelException.class,
         () ->
             operations.alterSemanticModel(
-                IDENT, SemanticModelChange.updateComment("Not implemented")));
-    assertThrows(UnsupportedOperationException.class, () -> 
operations.dropSemanticModel(IDENT));
+                IDENT,
+                SemanticModelChange.setProperty("key", "must-not-persist"),
+                (SemanticModelChange) null));
+
+    assertSame(original, operations.loadSemanticModel(IDENT));
+    assertEquals(Map.of("key", "old"), 
operations.loadSemanticModel(IDENT).properties());
+  }
+
+  @Test
+  public void testMetadataAndPropertyChangesDoNotRunWriteValidation() throws 
IOException {
+    InMemoryEntityStore store = new InMemoryEntityStore();
+    SemanticModelDefinition malformedDefinition = 
mock(SemanticModelDefinition.class);
+    store.put(entity(malformedDefinition), false);
+    @SuppressWarnings("unchecked")
+    BiConsumer<NameIdentifier, SemanticModelDefinition> writeValidator = 
mock(BiConsumer.class);
+    ManagedSemanticModelOperations operations =
+        new ManagedSemanticModelOperations(store, new RandomIdGenerator(), 
writeValidator);
+
+    SemanticModel propertyUpdated =
+        operations.alterSemanticModel(IDENT, 
SemanticModelChange.setProperty("safe", "true"));
+    assertEquals(Map.of("safe", "true"), propertyUpdated.properties());
+
+    SemanticModel commentUpdated =
+        operations.alterSemanticModel(IDENT, 
SemanticModelChange.updateComment("Metadata only"));
+    assertEquals("Metadata only", commentUpdated.comment());
+
+    NameIdentifier renamedIdent = NameIdentifier.of(NAMESPACE, 
"metadata_only_rename");
+    SemanticModel renamed =
+        operations.alterSemanticModel(IDENT, 
SemanticModelChange.rename(renamedIdent.name()));
+    assertEquals(renamedIdent.name(), renamed.name());
+    assertEquals(malformedDefinition, renamed.definition());
+    assertThrows(NoSuchSemanticModelException.class, () -> 
operations.loadSemanticModel(IDENT));
+    assertSame(renamed, operations.loadSemanticModel(renamedIdent));
+    verifyNoInteractions(writeValidator);
+  }
+
+  @Test
+  public void testInvalidChangesAreTypedAndDoNotReachTheStore() {
+    EntityStore store = mock(EntityStore.class);
+    ManagedSemanticModelOperations operations =
+        new ManagedSemanticModelOperations(
+            store, mock(IdGenerator.class), (ident, definition) -> {});
+    SemanticModelChange unsupported = new SemanticModelChange() {};
+
+    assertThrows(
+        IllegalSemanticModelException.class,
+        () -> operations.alterSemanticModel(IDENT, (SemanticModelChange[]) 
null));
+    assertThrows(IllegalSemanticModelException.class, () -> 
operations.alterSemanticModel(IDENT));
+    assertThrows(
+        IllegalSemanticModelException.class,
+        () -> operations.alterSemanticModel(IDENT, (SemanticModelChange) 
null));
+    assertThrows(
+        IllegalSemanticModelException.class,
+        () -> operations.alterSemanticModel(IDENT, unsupported));
+    verifyNoInteractions(store);
+  }
+
+  @Test
+  public void testLifecycleExceptionMapping() throws IOException {
+    EntityStore store = mock(EntityStore.class);
+    ManagedSemanticModelOperations operations =
+        new ManagedSemanticModelOperations(
+            store, mock(IdGenerator.class), (ident, definition) -> {});
+
+    when(store.list(NAMESPACE, SemanticModelEntity.class, 
Entity.EntityType.SEMANTIC_MODEL))
+        .thenThrow(new NoSuchEntityException("Missing schema"));
+    assertThrows(NoSuchSchemaException.class, () -> 
operations.listSemanticModels(NAMESPACE));
+    IOException listFailure = new IOException("List failed");
+    doThrow(listFailure)
+        .when(store)
+        .list(NAMESPACE, SemanticModelEntity.class, 
Entity.EntityType.SEMANTIC_MODEL);
+    assertSame(
+        listFailure,
+        assertThrows(RuntimeException.class, () -> 
operations.listSemanticModels(NAMESPACE))
+            .getCause());
+
+    doThrow(new NoSuchEntityException("Missing model"))
+        .when(store)
+        .update(
+            eq(IDENT), eq(SemanticModelEntity.class), 
eq(Entity.EntityType.SEMANTIC_MODEL), any());
+    assertThrows(
+        NoSuchSemanticModelException.class,
+        () ->
+            operations.alterSemanticModel(IDENT, 
SemanticModelChange.setProperty("key", "value")));
+    doThrow(new EntityAlreadyExistsException("Rename conflict"))
+        .when(store)
+        .update(
+            eq(IDENT), eq(SemanticModelEntity.class), 
eq(Entity.EntityType.SEMANTIC_MODEL), any());
+    assertThrows(
+        SemanticModelAlreadyExistsException.class,
+        () -> operations.alterSemanticModel(IDENT, 
SemanticModelChange.rename("conflict")));
+    IOException updateFailure = new IOException("Update failed");
+    doThrow(updateFailure)
+        .when(store)
+        .update(
+            eq(IDENT), eq(SemanticModelEntity.class), 
eq(Entity.EntityType.SEMANTIC_MODEL), any());
+    assertSame(
+        updateFailure,
+        assertThrows(
+                RuntimeException.class,
+                () ->
+                    operations.alterSemanticModel(IDENT, 
SemanticModelChange.removeProperty("key")))
+            .getCause());
+
+    when(store.delete(IDENT, Entity.EntityType.SEMANTIC_MODEL))
+        .thenThrow(new NoSuchEntityException("Missing model"));
+    assertFalse(operations.dropSemanticModel(IDENT));
+    IOException dropFailure = new IOException("Drop failed");
+    doThrow(dropFailure).when(store).delete(IDENT, 
Entity.EntityType.SEMANTIC_MODEL);
+    assertSame(
+        dropFailure,
+        assertThrows(RuntimeException.class, () -> 
operations.dropSemanticModel(IDENT)).getCause());
   }
 
   private static RuntimeException assertCreateFailure(
@@ -202,12 +492,23 @@ public class TestManagedSemanticModelOperations {
     return thrown;
   }
 
+  private static void assertCreationAuditPreserved(SemanticModel original, 
SemanticModel altered) {
+    assertEquals(original.auditInfo().creator(), 
altered.auditInfo().creator());
+    assertEquals(original.auditInfo().createTime(), 
altered.auditInfo().createTime());
+    assertEquals(original.auditInfo().creator(), 
altered.auditInfo().lastModifier());
+    assertNotNull(altered.auditInfo().lastModifiedTime());
+  }
+
   private static SemanticModelEntity entity() {
+    return entity(definition("orders"));
+  }
+
+  private static SemanticModelEntity entity(SemanticModelDefinition 
definition) {
     return SemanticModelEntity.builder()
         .withId(1L)
         .withName(IDENT.name())
         .withNamespace(IDENT.namespace())
-        .withDefinition(definition("orders"))
+        .withDefinition(definition)
         .withAuditInfo(AuditInfo.EMPTY)
         .build();
   }
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestManagedSemanticModelOperationsJDBC.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestManagedSemanticModelOperationsJDBC.java
index c6b10f7c31..01871a9d1b 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestManagedSemanticModelOperationsJDBC.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestManagedSemanticModelOperationsJDBC.java
@@ -18,8 +18,12 @@
  */
 package org.apache.gravitino.catalog;
 
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.IOException;
@@ -33,11 +37,14 @@ import org.apache.gravitino.GravitinoEnv;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.Namespace;
 import org.apache.gravitino.cache.NoOpsCache;
+import org.apache.gravitino.exceptions.IllegalSemanticModelException;
+import org.apache.gravitino.exceptions.SemanticModelAlreadyExistsException;
 import org.apache.gravitino.semantic.CustomExtension;
 import org.apache.gravitino.semantic.Dataset;
 import org.apache.gravitino.semantic.Metric;
 import org.apache.gravitino.semantic.Relationship;
 import org.apache.gravitino.semantic.SemanticModel;
+import org.apache.gravitino.semantic.SemanticModelChange;
 import org.apache.gravitino.semantic.SemanticModelDefinition;
 import org.apache.gravitino.storage.RandomIdGenerator;
 import org.apache.gravitino.storage.relational.RelationalEntityStore;
@@ -85,7 +92,10 @@ public class TestManagedSemanticModelOperationsJDBC extends 
TestJDBCBackend {
         new ManagedSemanticModelOperations(
             store,
             RandomIdGenerator.INSTANCE,
-            (ident, definition) -> writeValidationCount.incrementAndGet());
+            (ident, definition) -> {
+              SemanticModelValidator.validateDefinition(definition);
+              writeValidationCount.incrementAndGet();
+            });
   }
 
   @AfterEach
@@ -95,21 +105,7 @@ public class TestManagedSemanticModelOperationsJDBC extends 
TestJDBCBackend {
 
   @TestTemplate
   public void testCreateThenLoadRoundTrip() throws IOException {
-    Dataset dataset =
-        Dataset.builder()
-            .withName("orders")
-            .withSource(NameIdentifier.of("source_catalog", "source_schema", 
"orders"))
-            .withPrimaryKey(new String[0])
-            .withUniqueKeys(new String[0][])
-            .withCustomExtensions(new CustomExtension[0])
-            .build();
-    SemanticModelDefinition definition =
-        SemanticModelDefinition.builder()
-            .withDatasets(new Dataset[] {dataset})
-            .withRelationships(new Relationship[0])
-            .withMetrics((Metric[]) null)
-            .withCustomExtensions(new CustomExtension[0])
-            .build();
+    SemanticModelDefinition definition = definition("orders");
 
     SemanticModel created =
         operations.createSemanticModel(
@@ -122,4 +118,91 @@ public class TestManagedSemanticModelOperationsJDBC 
extends TestJDBCBackend {
     assertEquals(1, writeValidationCount.get());
     assertTrue(backend.exists(modelIdent, Entity.EntityType.SEMANTIC_MODEL));
   }
+
+  @TestTemplate
+  public void testListAlterAndDropRoundTrip() throws IOException {
+    SemanticModel created =
+        operations.createSemanticModel(
+            modelIdent, "Original", definition("orders"), Map.of("domain", 
"sales"));
+    NameIdentifier renamedIdent = NameIdentifier.of(modelIdent.namespace(), 
"renamed_sales_model");
+    SemanticModelDefinition replacement = definition("invoices");
+
+    SemanticModel altered =
+        operations.alterSemanticModel(
+            modelIdent,
+            SemanticModelChange.rename(renamedIdent.name()),
+            SemanticModelChange.updateComment("Updated"),
+            SemanticModelChange.removeProperty("domain"),
+            SemanticModelChange.setProperty("tier", "gold"),
+            SemanticModelChange.replaceDefinition(replacement));
+
+    assertEquals(renamedIdent.name(), altered.name());
+    assertEquals("Updated", altered.comment());
+    assertEquals(replacement, altered.definition());
+    assertEquals(Map.of("tier", "gold"), altered.properties());
+    assertEquals(created.auditInfo().creator(), altered.auditInfo().creator());
+    assertEquals(created.auditInfo().createTime(), 
altered.auditInfo().createTime());
+    assertEquals(created.auditInfo().creator(), 
altered.auditInfo().lastModifier());
+    assertNotNull(altered.auditInfo().lastModifiedTime());
+    assertEquals(2, writeValidationCount.get());
+    assertArrayEquals(
+        new NameIdentifier[] {renamedIdent}, 
operations.listSemanticModels(modelIdent.namespace()));
+    assertFalse(backend.exists(modelIdent, Entity.EntityType.SEMANTIC_MODEL));
+    assertTrue(backend.exists(renamedIdent, Entity.EntityType.SEMANTIC_MODEL));
+    assertEquals(altered, operations.loadSemanticModel(renamedIdent));
+    assertTrue(operations.dropSemanticModel(renamedIdent));
+    assertFalse(operations.dropSemanticModel(renamedIdent));
+  }
+
+  @TestTemplate
+  public void testRejectedAlterIsAtomicAndRenameConflictIsTyped() {
+    SemanticModel original =
+        operations.createSemanticModel(
+            modelIdent, "Original", definition("orders"), Map.of("owner", 
"sales"));
+    NameIdentifier existingIdent = NameIdentifier.of(modelIdent.namespace(), 
"existing_model");
+    operations.createSemanticModel(existingIdent, null, 
definition("customers"), Map.of());
+    SemanticModelDefinition duplicateDatasets =
+        SemanticModelDefinition.builder()
+            .withDatasets(
+                new Dataset[] {
+                  definition("duplicate").datasets()[0], 
definition("duplicate").datasets()[0]
+                })
+            .build();
+
+    assertThrows(
+        IllegalSemanticModelException.class,
+        () ->
+            operations.alterSemanticModel(
+                modelIdent,
+                SemanticModelChange.updateComment("Must not persist"),
+                SemanticModelChange.setProperty("owner", "changed"),
+                SemanticModelChange.replaceDefinition(duplicateDatasets)));
+    assertThrows(
+        SemanticModelAlreadyExistsException.class,
+        () ->
+            operations.alterSemanticModel(
+                modelIdent, SemanticModelChange.rename(existingIdent.name())));
+
+    SemanticModel loaded = operations.loadSemanticModel(modelIdent);
+    assertEquals(original, loaded);
+    assertEquals("existing_model", 
operations.loadSemanticModel(existingIdent).name());
+    assertEquals(2, writeValidationCount.get());
+  }
+
+  private static SemanticModelDefinition definition(String datasetName) {
+    Dataset dataset =
+        Dataset.builder()
+            .withName(datasetName)
+            .withSource(NameIdentifier.of("source_catalog", "source_schema", 
datasetName))
+            .withPrimaryKey(new String[0])
+            .withUniqueKeys(new String[0][])
+            .withCustomExtensions(new CustomExtension[0])
+            .build();
+    return SemanticModelDefinition.builder()
+        .withDatasets(new Dataset[] {dataset})
+        .withRelationships(new Relationship[0])
+        .withMetrics((Metric[]) null)
+        .withCustomExtensions(new CustomExtension[0])
+        .build();
+  }
 }
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestSemanticModelOperationDispatcher.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestSemanticModelOperationDispatcher.java
index 9d0c2aa53d..f4be170d51 100644
--- 
a/core/src/test/java/org/apache/gravitino/catalog/TestSemanticModelOperationDispatcher.java
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestSemanticModelOperationDispatcher.java
@@ -21,10 +21,12 @@ 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.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -41,6 +43,7 @@ import org.apache.gravitino.Namespace;
 import org.apache.gravitino.Schema;
 import org.apache.gravitino.exceptions.IllegalSemanticModelException;
 import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.exceptions.NoSuchSemanticModelException;
 import org.apache.gravitino.lock.LockManager;
 import org.apache.gravitino.secret.SecretManager;
 import org.apache.gravitino.semantic.Dataset;
@@ -131,16 +134,83 @@ public class TestSemanticModelOperationDispatcher {
   }
 
   @Test
-  public void testRemainingManagedCapabilitiesStayUnsupported() {
-    assertThrows(
-        UnsupportedOperationException.class, () -> 
dispatcher.listSemanticModels(NAMESPACE));
+  public void testListAlterAndDropLifecycleWithDefinitionValidation() {
+    dispatcher.createSemanticModel(MODEL_IDENT, "Original", validDefinition(), 
Map.of());
+
+    SemanticModel propertyUpdated =
+        dispatcher.alterSemanticModel(
+            MODEL_IDENT, SemanticModelChange.setProperty("owner", 
"analytics"));
+    SemanticModel renamed =
+        dispatcher.alterSemanticModel(
+            MODEL_IDENT,
+            SemanticModelChange.rename("renamed_sales_model"),
+            SemanticModelChange.updateComment("Updated"));
+    NameIdentifier renamedIdent = NameIdentifier.of(NAMESPACE, renamed.name());
+    assertEquals(Map.of("owner", "analytics"), propertyUpdated.properties());
+    assertEquals("Updated", renamed.comment());
+
+    SemanticModel replaced =
+        dispatcher.alterSemanticModel(
+            renamedIdent, 
SemanticModelChange.replaceDefinition(validDefinition()));
+    assertEquals(2, replaced.definition().datasets().length);
+    assertArrayEquals(
+        new NameIdentifier[] {renamedIdent}, 
dispatcher.listSemanticModels(NAMESPACE));
+    assertTrue(dispatcher.dropSemanticModel(renamedIdent));
+    assertFalse(dispatcher.dropSemanticModel(renamedIdent));
+  }
+
+  @Test
+  public void testRejectedDefinitionReplacementDoesNotPersistOtherChanges() {
+    SemanticModel original =
+        dispatcher.createSemanticModel(
+            MODEL_IDENT, "Original", validDefinition(), Map.of("owner", 
"sales"));
+
+    SemanticModelDefinition invalidReplacement =
+        SemanticModelDefinition.builder()
+            .withDatasets(
+                new Dataset[] {
+                  dataset("duplicate", "orders", null, null),
+                  dataset("duplicate", "customers", null, null)
+                })
+            .withRelationships(new Relationship[0])
+            .build();
+
     assertThrows(
-        UnsupportedOperationException.class,
+        IllegalSemanticModelException.class,
         () ->
             dispatcher.alterSemanticModel(
-                MODEL_IDENT, SemanticModelChange.updateComment("Not 
implemented")));
+                MODEL_IDENT,
+                SemanticModelChange.rename("must_not_persist"),
+                SemanticModelChange.updateComment("Must not persist"),
+                SemanticModelChange.setProperty("owner", "changed"),
+                SemanticModelChange.replaceDefinition(invalidReplacement)));
+
+    SemanticModel loaded = dispatcher.loadSemanticModel(MODEL_IDENT);
+    assertEquals(original.name(), loaded.name());
+    assertEquals(original.comment(), loaded.comment());
+    assertEquals(original.properties(), loaded.properties());
+    assertEquals(original.definition(), loaded.definition());
+    assertFalse(dispatcher.semanticModelExists(NameIdentifier.of(NAMESPACE, 
"must_not_persist")));
+  }
+
+  @Test
+  public void testLifecycleMissingParentAndInvalidChangeSemantics() {
     assertThrows(
-        UnsupportedOperationException.class, () -> 
dispatcher.dropSemanticModel(MODEL_IDENT));
+        IllegalSemanticModelException.class,
+        () -> dispatcher.alterSemanticModel(MODEL_IDENT, 
(SemanticModelChange[]) null));
+    assertThrows(
+        IllegalSemanticModelException.class, () -> 
dispatcher.alterSemanticModel(MODEL_IDENT));
+
+    when(schemaDispatcher.loadSchema(SCHEMA_IDENT))
+        .thenThrow(new NoSuchSchemaException("Schema does not exist"));
+    assertThrows(NoSuchSchemaException.class, () -> 
dispatcher.listSemanticModels(NAMESPACE));
+    when(schemaDispatcher.schemaExists(SCHEMA_IDENT)).thenReturn(false);
+    assertThrows(
+        NoSuchSemanticModelException.class,
+        () ->
+            dispatcher.alterSemanticModel(
+                MODEL_IDENT, SemanticModelChange.updateComment("Missing")));
+    assertFalse(dispatcher.dropSemanticModel(MODEL_IDENT));
   }
 
   @Test

Reply via email to