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 a853a14544 [#12594] fix(server): Check semantic source capabilities 
before lookup (#13763)
a853a14544 is described below

commit a853a1454449f79f1953a4d98b90b2fd021f64c6
Author: Akshay Thorat <[email protected]>
AuthorDate: Mon Oct 12 02:36:08 2026 -0700

    [#12594] fix(server): Check semantic source capabilities before lookup 
(#13763)
    
    ### What changes were proposed in this pull request?
    
    Follow up on the post-merge review of #13512:
    
    - Detect Table and View support in one catalog lookup and skip
    unsupported source operations, while preserving authorization checks and
    errors from supported connectors.
    - Load the target Semantic Model before validating replacement sources,
    so a missing model returns 404 without looking up its sources.
    - Add HTTP and validator regressions, and remove a stale import test
    assertion that prevents current main from compiling.
    
    ### Why are the changes needed?
    
    A source in a catalog supporting neither Tables nor Views currently
    returns 501 rather than 400. Replacing the definition of a nonexistent
    model can return a source error (400 or 502) instead of 404.
    
    Related to #12594 and
    https://github.com/apache/gravitino/pull/13512#pullrequestreview-5486111328.
    
    Fix: #12594
    
    ### Does this PR introduce _any_ user-facing change?
    
    The affected requests return the appropriate 400 or 404 response. No
    public API or configuration changes.
    
    ### How was this patch tested?
    
    Five HTTP regression cases failed before the fix: three expected 400 but
    received 501, and two expected 404 but received 400/502. They pass after
    the fix. Additional coverage checks restricted readers, all Table/View
    capability combinations, view-only resolution, and propagation of
    supported connector failures.
    
    `JAVA_HOME=$(/usr/libexec/java_home -v 17) ./gradlew :server:check
    :server:javadoc -PskipITs`
    
    All 652 server tests passed, as did formatting, server checks, and
    Javadoc generation. Docker integration tests were not run.
---
 .../server/web/rest/SemanticModelOperations.java   |   5 +
 .../web/rest/SemanticModelSourceValidator.java     |  57 ++++++----
 .../web/rest/TestSemanticModelOperations.java      | 117 +++++++++++++++++++--
 .../web/rest/TestSemanticModelSourceValidator.java |  68 +++++++++---
 4 files changed, 203 insertions(+), 44 deletions(-)

diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelOperations.java
index d75cb5dc91..5e2227bf63 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelOperations.java
@@ -20,6 +20,7 @@ package org.apache.gravitino.server.web.rest;
 
 import com.codahale.metrics.annotation.ResponseMetered;
 import com.codahale.metrics.annotation.Timed;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Locale;
 import javax.inject.Inject;
@@ -354,6 +355,10 @@ public class SemanticModelOperations {
                 request.getUpdates().stream()
                     .map(SemanticModelUpdateRequest::semanticModelChange)
                     .toArray(SemanticModelChange[]::new);
+            if (Arrays.stream(changes)
+                .anyMatch(change -> change instanceof 
SemanticModelChange.ReplaceDefinition)) {
+              dispatcher.loadSemanticModel(ident);
+            }
             for (SemanticModelChange change : changes) {
               if (change instanceof SemanticModelChange.ReplaceDefinition) {
                 sourceValidator.validate(
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelSourceValidator.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelSourceValidator.java
index 6b26e7e50a..b77eab345c 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelSourceValidator.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelSourceValidator.java
@@ -19,11 +19,12 @@
 package org.apache.gravitino.server.web.rest;
 
 import java.util.Arrays;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
-import java.util.function.Predicate;
+import java.util.function.Function;
 import java.util.function.Supplier;
 import javax.inject.Inject;
 import org.apache.gravitino.Entity;
@@ -33,10 +34,12 @@ import 
org.apache.gravitino.authorization.AuthorizationRequestContext;
 import org.apache.gravitino.authorization.GravitinoAuthorizer;
 import org.apache.gravitino.catalog.TableDispatcher;
 import org.apache.gravitino.catalog.ViewDispatcher;
+import org.apache.gravitino.connector.CatalogOperations;
 import org.apache.gravitino.exceptions.ForbiddenException;
 import org.apache.gravitino.exceptions.IllegalSemanticModelException;
 import org.apache.gravitino.exceptions.NotFoundException;
 import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.TableCatalog;
 import org.apache.gravitino.rel.ViewCatalog;
 import org.apache.gravitino.semantic.Dataset;
 import org.apache.gravitino.semantic.Relationship;
@@ -51,7 +54,7 @@ public class SemanticModelSourceValidator {
   private final TableDispatcher tables;
   private final ViewDispatcher views;
   private final Supplier<GravitinoAuthorizer> authorizer;
-  private final Predicate<NameIdentifier> supportsViews;
+  private final Function<NameIdentifier, Set<Entity.EntityType>> 
supportedSourceTypes;
 
   /**
    * Creates a validator using the server's authorizer, including its 
disabled-mode pass-through.
@@ -75,15 +78,25 @@ public class SemanticModelSourceValidator {
                 .catalogManager()
                 .doWithCatalog(
                     NameIdentifierUtil.getCatalogIdentifier(ident),
-                    catalog -> catalog.ops() instanceof ViewCatalog));
+                    catalog -> {
+                      CatalogOperations ops = catalog.ops();
+                      Set<Entity.EntityType> types = 
EnumSet.noneOf(Entity.EntityType.class);
+                      if (ops instanceof TableCatalog) {
+                        types.add(Entity.EntityType.TABLE);
+                      }
+                      if (ops instanceof ViewCatalog) {
+                        types.add(Entity.EntityType.VIEW);
+                      }
+                      return types;
+                    }));
   }
 
   SemanticModelSourceValidator(
       TableDispatcher tables,
       ViewDispatcher views,
       Supplier<GravitinoAuthorizer> authorizer,
-      Predicate<NameIdentifier> supportsViews) {
-    this.supportsViews = supportsViews;
+      Function<NameIdentifier, Set<Entity.EntityType>> supportedSourceTypes) {
+    this.supportedSourceTypes = supportedSourceTypes;
     this.tables = tables;
     this.views = views;
     this.authorizer = authorizer;
@@ -144,13 +157,6 @@ public class SemanticModelSourceValidator {
             
AuthorizationExpressionConstants.LOAD_TABLE_AUTHORIZATION_EXPRESSION,
             currentAuthorizer,
             context);
-    if (tableAllowed) {
-      try {
-        return columnNames(ident, tables.loadTable(ident).columns());
-      } catch (NotFoundException missing) {
-        // The untyped source can also refer to a logical view. Authorize that 
lookup separately.
-      }
-    }
     boolean viewAllowed =
         canLoad(
             ident,
@@ -158,18 +164,27 @@ public class SemanticModelSourceValidator {
             
AuthorizationExpressionConstants.LOAD_VIEW_AUTHORIZATION_EXPRESSION,
             currentAuthorizer,
             context);
-    boolean viewSupported = true;
-    if (tableAllowed || viewAllowed) {
-      try {
-        viewSupported = supportsViews.test(ident);
-        if (viewAllowed && viewSupported) {
-          return columnNames(ident, views.loadView(ident).columns());
+    if (!tableAllowed && !viewAllowed) {
+      throw new ForbiddenException("Not authorized to resolve Semantic Model 
source %s", ident);
+    }
+    Set<Entity.EntityType> supported = EnumSet.of(Entity.EntityType.TABLE, 
Entity.EntityType.VIEW);
+    try {
+      supported = supportedSourceTypes.apply(ident);
+      if (tableAllowed && supported.contains(Entity.EntityType.TABLE)) {
+        try {
+          return columnNames(ident, tables.loadTable(ident).columns());
+        } catch (NotFoundException missing) {
+          // An untyped source can also refer to a view.
         }
-      } catch (NotFoundException missing) {
-        // Do not disclose whether the other, inaccessible object type exists.
       }
+      if (viewAllowed && supported.contains(Entity.EntityType.VIEW)) {
+        return columnNames(ident, views.loadView(ident).columns());
+      }
+    } catch (NotFoundException missing) {
+      // Do not disclose whether the other, inaccessible object type exists.
     }
-    if (!tableAllowed || (viewSupported && !viewAllowed)) {
+    if ((supported.contains(Entity.EntityType.TABLE) && !tableAllowed)
+        || (supported.contains(Entity.EntityType.VIEW) && !viewAllowed)) {
       throw new ForbiddenException("Not authorized to resolve Semantic Model 
source %s", ident);
     }
     throw new IllegalSemanticModelException("Semantic Model source %s does not 
exist", ident);
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestSemanticModelOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestSemanticModelOperations.java
index c5d7f6b098..0856bd1879 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestSemanticModelOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestSemanticModelOperations.java
@@ -26,6 +26,7 @@ import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -41,6 +42,7 @@ import java.net.http.HttpRequest;
 import java.net.http.HttpResponse;
 import java.time.Duration;
 import java.time.Instant;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.IntStream;
@@ -135,6 +137,7 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
   private final SemanticModelDispatcher dispatcher = 
mock(SemanticModelDispatcher.class);
   private final TableDispatcher tables = mock(TableDispatcher.class);
   private final ViewDispatcher views = mock(ViewDispatcher.class);
+  private boolean tablesSupported = true;
   private boolean viewsSupported = true;
   private GravitinoAuthorizer sourceAuthorizer = new PassThroughAuthorizer();
   private final String metalake = "semantic_model_metalake";
@@ -160,7 +163,15 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
           protected void configure() {
             bind(dispatcher).to(SemanticModelDispatcher.class).ranked(2);
             bind(new SemanticModelSourceValidator(
-                    tables, views, () -> sourceAuthorizer, ident -> 
viewsSupported))
+                    tables,
+                    views,
+                    () -> sourceAuthorizer,
+                    ident -> {
+                      EnumSet<EntityType> types = 
EnumSet.noneOf(EntityType.class);
+                      if (tablesSupported) types.add(EntityType.TABLE);
+                      if (viewsSupported) types.add(EntityType.VIEW);
+                      return types;
+                    }))
                 .to(SemanticModelSourceValidator.class);
             
bindFactory(MockServletRequestFactory.class).to(HttpServletRequest.class);
           }
@@ -171,6 +182,7 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
   @BeforeEach
   void resetDispatcher() {
     reset(dispatcher, tables, views);
+    tablesSupported = true;
     viewsSupported = true;
     sourceAuthorizer = new PassThroughAuthorizer();
     Table table = mock(Table.class);
@@ -178,6 +190,7 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
     when(column.name()).thenReturn("order_id");
     when(table.columns()).thenReturn(new Column[] {column});
     when(tables.loadTable(any())).thenReturn(table);
+    
when(dispatcher.loadSemanticModel(any())).thenReturn(semanticModel("sales", 
null));
     doCallRealMethod().when(dispatcher).exportOssieDocument(any(), any());
   }
 
@@ -215,7 +228,11 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
     SemanticModelOperations operations =
         new SemanticModelOperations(
             dispatcher,
-            new SemanticModelSourceValidator(tables, views, () -> 
sourceAuthorizer, ident -> true));
+            new SemanticModelSourceValidator(
+                tables,
+                views,
+                () -> sourceAuthorizer,
+                ident -> EnumSet.of(EntityType.TABLE, EntityType.VIEW)));
     FieldUtils.writeField(operations, "httpRequest", 
mock(HttpServletRequest.class), true);
     try (MockedStatic<MetadataAuthzHelper> helper = 
mockStatic(MetadataAuthzHelper.class)) {
       helper
@@ -286,7 +303,8 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
         code,
         type.getSimpleName(),
         message);
-    verifyNoInteractions(dispatcher, views);
+    verifyNoModelWrites();
+    verifyNoInteractions(views);
     if (failure.equals("denied")) {
       verifyNoInteractions(tables);
     }
@@ -682,7 +700,7 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
         ErrorConstants.ILLEGAL_ARGUMENTS_CODE,
         IllegalSemanticModelException.class.getSimpleName(),
         "does not exist");
-    verifyNoInteractions(dispatcher);
+    verifyNoModelWrites();
   }
 
   @ParameterizedTest
@@ -703,7 +721,8 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
         ErrorConstants.ILLEGAL_ARGUMENTS_CODE,
         IllegalSemanticModelException.class.getSimpleName(),
         "does not exist");
-    verifyNoInteractions(dispatcher, views);
+    verifyNoModelWrites();
+    verifyNoInteractions(views);
   }
 
   @ParameterizedTest
@@ -758,7 +777,75 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
           "does not exist");
     }
     verify(tables).loadTable(NameIdentifier.of(metalake, catalog, schema, 
"orders"));
-    verifyNoInteractions(views, dispatcher);
+    verifyNoModelWrites();
+    verifyNoInteractions(views);
+  }
+
+  @ParameterizedTest
+  @CsvSource({
+    "create,false",
+    "replace,false",
+    "import,false",
+    "create,true",
+    "replace,true",
+    "import,true"
+  })
+  void testSourceInCatalogWithoutTablesOrViews(String operation, boolean 
tableOnlyReader) {
+    if (tableOnlyReader) {
+      sourceAuthorizer = mock(GravitinoAuthorizer.class);
+      when(sourceAuthorizer.authorize(any(), any(), any(), any(), any()))
+          .thenAnswer(
+              invocation -> {
+                Privilege.Name privilege = invocation.getArgument(3);
+                return privilege == Privilege.Name.USE_CATALOG
+                    || privilege == Privilege.Name.USE_SCHEMA
+                    || privilege == Privilege.Name.SELECT_TABLE;
+              });
+    }
+    tablesSupported = false;
+    viewsSupported = false;
+    when(tables.loadTable(any()))
+        .thenThrow(new UnsupportedOperationException("Catalog does not support 
table operations"));
+    Response response =
+        operation.equals("import")
+            ? postDocument(
+                semanticModelPath() + "/ossie",
+                "version: 0.2.0.dev0\nname: sales\ndatasets:\n  - name: 
orders\n    source: "
+                    + catalog
+                    + "."
+                    + schema
+                    + ".orders\n",
+                "application/yaml")
+            : writeDefinition(operation.equals("replace"), 
semanticModelDefinition());
+    assertError(
+        response,
+        Response.Status.BAD_REQUEST,
+        ErrorConstants.ILLEGAL_ARGUMENTS_CODE,
+        IllegalSemanticModelException.class.getSimpleName(),
+        "does not exist");
+    verifyNoInteractions(tables, views);
+    verifyNoModelWrites();
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {false, true})
+  void testMissingModelBeforeSourceValidation(boolean unavailableSource) {
+    when(dispatcher.loadSemanticModel(semanticModelIdentifier("sales")))
+        .thenThrow(new NoSuchSemanticModelException("sales does not exist"));
+    when(tables.loadTable(any()))
+        .thenThrow(
+            unavailableSource
+                ? new ConnectionFailedException("offline")
+                : new NoSuchTableException("missing"));
+    when(views.loadView(any())).thenThrow(new NoSuchViewException("missing"));
+    assertError(
+        writeDefinition(true, semanticModelDefinition()),
+        Response.Status.NOT_FOUND,
+        ErrorConstants.NOT_FOUND_CODE,
+        NoSuchSemanticModelException.class.getSimpleName(),
+        "sales does not exist");
+    verifyNoInteractions(tables, views);
+    verifyNoModelWrites();
   }
 
   @ParameterizedTest
@@ -773,7 +860,7 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
         ErrorConstants.ILLEGAL_ARGUMENTS_CODE,
         IllegalSemanticModelException.class.getSimpleName(),
         "order_id");
-    verifyNoInteractions(dispatcher);
+    verifyNoModelWrites();
   }
 
   @ParameterizedTest
@@ -786,7 +873,8 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
         ErrorConstants.FORBIDDEN_CODE,
         ForbiddenException.class.getSimpleName(),
         "Not authorized");
-    verifyNoInteractions(tables, views, dispatcher);
+    verifyNoModelWrites();
+    verifyNoInteractions(tables, views);
   }
 
   @ParameterizedTest
@@ -799,7 +887,8 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
         ErrorConstants.CONNECTION_FAILED_CODE,
         ConnectionFailedException.class.getSimpleName(),
         "source unavailable");
-    verifyNoInteractions(views, dispatcher);
+    verifyNoModelWrites();
+    verifyNoInteractions(views);
   }
 
   @ParameterizedTest
@@ -841,7 +930,8 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
           ErrorConstants.ILLEGAL_ARGUMENTS_CODE,
           IllegalArgumentException.class.getSimpleName(),
           "Metadata object full name cannot be blank");
-      verifyNoInteractions(tables, views, dispatcher);
+      verifyNoModelWrites();
+      verifyNoInteractions(tables, views);
       return;
     }
     if (sourceExists) {
@@ -861,7 +951,7 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
           IllegalSemanticModelException.class.getSimpleName(),
           "does not exist");
       verify(views).loadView(fullSource);
-      verifyNoInteractions(dispatcher);
+      verifyNoModelWrites();
     }
     verify(tables).loadTable(fullSource);
   }
@@ -884,6 +974,11 @@ public class TestSemanticModelOperations extends 
BaseOperationsTest {
                                                         segment, replace, 
authorized, exists)))));
   }
 
+  private void verifyNoModelWrites() {
+    verify(dispatcher, never()).createSemanticModel(any(), any(), any(), 
any());
+    verify(dispatcher, never()).alterSemanticModel(any(), 
any(SemanticModelChange[].class));
+  }
+
   private Response writeDefinition(boolean replaceDefinition, 
SemanticModelDefinition definition) {
     return replaceDefinition
         ? put(
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestSemanticModelSourceValidator.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestSemanticModelSourceValidator.java
index aa7544f830..3395c8827b 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestSemanticModelSourceValidator.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestSemanticModelSourceValidator.java
@@ -32,7 +32,10 @@ import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.withSettings;
 
-import java.util.function.Predicate;
+import java.util.EnumSet;
+import java.util.Set;
+import java.util.function.Function;
+import org.apache.gravitino.Entity.EntityType;
 import org.apache.gravitino.GravitinoEnv;
 import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.NameIdentifier;
@@ -50,6 +53,7 @@ import org.apache.gravitino.exceptions.NoSuchTableException;
 import org.apache.gravitino.exceptions.NoSuchViewException;
 import org.apache.gravitino.rel.Column;
 import org.apache.gravitino.rel.Table;
+import org.apache.gravitino.rel.TableCatalog;
 import org.apache.gravitino.rel.View;
 import org.apache.gravitino.rel.ViewCatalog;
 import org.apache.gravitino.semantic.Dataset;
@@ -59,7 +63,7 @@ import 
org.apache.gravitino.server.authorization.PassThroughAuthorizer;
 import org.apache.gravitino.utils.ThrowableFunction;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.ValueSource;
+import org.junit.jupiter.params.provider.CsvSource;
 import org.mockito.MockedStatic;
 
 /** Tests catalog-backed validation independently of model persistence. */
@@ -73,7 +77,8 @@ public class TestSemanticModelSourceValidator {
   private final ViewDispatcher views = mock(ViewDispatcher.class);
   private GravitinoAuthorizer authorizer = new PassThroughAuthorizer();
   private final SemanticModelSourceValidator validator =
-      new SemanticModelSourceValidator(tables, views, () -> authorizer, ident 
-> true);
+      new SemanticModelSourceValidator(
+          tables, views, () -> authorizer, ident -> 
EnumSet.of(EntityType.TABLE, EntityType.VIEW));
 
   @Test
   void testCrossCatalogSourceAndKeysWithDisabledAuthorization() {
@@ -201,19 +206,25 @@ public class TestSemanticModelSourceValidator {
   }
 
   @ParameterizedTest
-  @ValueSource(booleans = {false, true})
-  void testViewSupportCheckedBeforeFallback(boolean supportsViews) {
+  @CsvSource({"false,false", "false,true", "true,false", "true,true"})
+  void testSourceSupportCheckedBeforeLookup(boolean supportsTables, boolean 
supportsViews) {
     CatalogManager catalogs = mock(CatalogManager.class);
     BaseCatalog catalog = mock(BaseCatalog.class);
+    Class<?>[] interfaces =
+        supportsTables
+            ? (supportsViews
+                ? new Class<?>[] {TableCatalog.class, ViewCatalog.class}
+                : new Class<?>[] {TableCatalog.class})
+            : (supportsViews ? new Class<?>[] {ViewCatalog.class} : new 
Class<?>[0]);
     CatalogOperations operations =
-        supportsViews
-            ? mock(CatalogOperations.class, 
withSettings().extraInterfaces(ViewCatalog.class))
-            : mock(CatalogOperations.class);
+        interfaces.length == 0
+            ? mock(CatalogOperations.class)
+            : mock(CatalogOperations.class, 
withSettings().extraInterfaces(interfaces));
     when(catalog.ops()).thenReturn(operations);
     when(catalogs.doWithCatalog(eq(NameIdentifier.of("metalake", 
"other_catalog")), any()))
         .thenAnswer(
             invocation -> {
-              ThrowableFunction<BaseCatalog, Boolean> callback = 
invocation.getArgument(1);
+              ThrowableFunction<BaseCatalog, Set<EntityType>> callback = 
invocation.getArgument(1);
               return callback.apply(catalog);
             });
     when(tables.loadTable(FULL_SOURCE)).thenThrow(new 
NoSuchTableException("missing"));
@@ -227,6 +238,11 @@ public class TestSemanticModelSourceValidator {
       assertThrows(
           IllegalSemanticModelException.class,
           () -> checking.validate("metalake", definition(dataset("orders", 
null, null))));
+      if (supportsTables) {
+        verify(tables).loadTable(FULL_SOURCE);
+      } else {
+        verifyNoInteractions(tables);
+      }
       if (supportsViews) {
         verify(views).loadView(FULL_SOURCE);
       } else {
@@ -236,6 +252,34 @@ public class TestSemanticModelSourceValidator {
     }
   }
 
+  @Test
+  void testViewOnlyCatalogCanResolveSourceWithoutTableLookup() {
+    authorizeOnly(MetadataObject.Type.VIEW, Privilege.Name.SELECT_VIEW);
+    View view = mock(View.class);
+    doReturn(columns("id")).when(view).columns();
+    when(views.loadView(FULL_SOURCE)).thenReturn(view);
+    SemanticModelSourceValidator checking =
+        new SemanticModelSourceValidator(
+            tables, views, () -> authorizer, ident -> 
EnumSet.of(EntityType.VIEW));
+    checking.validate("metalake", definition(dataset("orders", new String[] 
{"id"}, null)));
+    verifyNoInteractions(tables);
+    verify(views).loadView(FULL_SOURCE);
+    when(views.loadView(FULL_SOURCE)).thenThrow(new 
NoSuchViewException("missing"));
+    assertThrows(
+        IllegalSemanticModelException.class,
+        () -> checking.validate("metalake", definition(dataset("orders", null, 
null))));
+  }
+
+  @Test
+  void testSupportedTableOperationFailureIsNotHidden() {
+    when(tables.loadTable(FULL_SOURCE))
+        .thenThrow(new UnsupportedOperationException("connector failure"));
+    assertThrows(
+        UnsupportedOperationException.class,
+        () -> validator.validate("metalake", definition(dataset("orders", 
null, null))));
+    verifyNoInteractions(views);
+  }
+
   @Test
   void testSupportedViewOperationFailureIsNotHidden() {
     when(tables.loadTable(FULL_SOURCE)).thenThrow(new 
NoSuchTableException("missing"));
@@ -249,13 +293,13 @@ public class TestSemanticModelSourceValidator {
   @Test
   void testDeniedSourcesDoNotProbeCatalogCapabilities() {
     authorizer = mock(GravitinoAuthorizer.class);
-    Predicate<NameIdentifier> supportsViews = mock(Predicate.class);
+    Function<NameIdentifier, Set<EntityType>> supportedSourceTypes = 
mock(Function.class);
     SemanticModelSourceValidator checking =
-        new SemanticModelSourceValidator(tables, views, () -> authorizer, 
supportsViews);
+        new SemanticModelSourceValidator(tables, views, () -> authorizer, 
supportedSourceTypes);
     assertThrows(
         ForbiddenException.class,
         () -> checking.validate("metalake", definition(dataset("orders", null, 
null))));
-    verifyNoInteractions(tables, views, supportsViews);
+    verifyNoInteractions(tables, views, supportedSourceTypes);
   }
 
   private void authorizeOnly(MetadataObject.Type type, Privilege.Name 
privilege) {

Reply via email to