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