Repository: trinodb/trino (Java)
Date: 2020-01-26 11:33:43+00:00
Task type: Bug Fix

Issue:
SHOW CATALOGS fails with "Internal error" when all catalogs filtered out by access control
There always is a catalog. If nothing is installed, there is a `system` catalog.
However, `AccessControl` may legitimately filter it out.

Then --
```
presto:default> SHOW CATALOGS;
Query 20200123_103158_00002_4u426 failed: Internal error
java.lang.IllegalStateException
	at com.google.common.base.Preconditions.checkState(Preconditions.java:491)
	at io.prestosql.sql.analyzer.StatementAnalyzer$Visitor.visitValues(StatementAnalyzer.java:1477)
	at io.prestosql.sql.analyzer.StatementAnalyzer$Visitor.visitValues(StatementAnalyzer.java:287)
	at io.prestosql.sql.tree.Values.accept(Values.java:55)
	at io.prestosql.sql.tree.AstVisitor.process(AstVisitor.java:27)
	at io.prestosql.sql.analyzer.StatementAnalyzer$Visitor.process(StatementAnalyzer.java:302)
	at io.prestosql.sql.analyzer.StatementAnalyzer$Visitor.visitAliasedRelation(StatementAnalyzer.java:1082)
	at io.prestosql.sql.analyzer.StatementAnalyzer$Visitor.visitAliasedRelation(StatementAnalyzer.java:287)
	at io.prestosql.sql.tree.AliasedRelation.accept(AliasedRelation.java:71)
	at io.prestosql.sql.tree.AstVisitor.process(AstVisitor.java:27)
	at io.prestosql.sql.analyzer.StatementAnalyzer$Visitor.process(StatementAnalyzer.java:302)
	at io.prestosql.sql.analyzer.StatementAnalyzer$Visitor.analyzeFrom(StatementAnalyzer.java:2204)
	at io.prestosql.sql.analyzer.StatementAnalyzer$Visitor.visitQuerySpecification(StatementAnalyzer.java:1161)
	at io.prestosql.sql.analyzer.StatementAnalyzer$Visitor.visitQuerySpecification(StatementAnalyzer.java:287)
	at io.prestosql.sql.tree.QuerySpecification.accept(QuerySpecification.java:144)
	at io.prestosql.sql.tree.AstVisitor.process(AstVisitor.java:27)
	at io.prestosql.sql.analyzer.StatementAnalyzer$Visitor.process(StatementAnalyzer.java:302)
	at io.prestosql.sql.analyzer.StatementAnalyzer$Visitor.process(StatementAnalyzer.java:312)
	at io.prestosql.sql.analyzer.StatementAnalyzer$Visitor.visitQuery(StatementAnalyzer.java:853)
	at io.prestosql.sql.analyzer.StatementAnalyzer$Visitor.visitQuery(StatementAnalyzer.java:287)
	at io.prestosql.sql.tree.Query.accept(Query.java:107)
	at io.prestosql.sql.tree.AstVisitor.process(AstVisitor.java:27)
	at io.prestosql.sql.analyzer.StatementAnalyzer$Visitor.process(StatementAnalyzer.java:302)
	at io.prestosql.sql.analyzer.StatementAnalyzer.analyze(StatementAnalyzer.java:279)
	at io.prestosql.sql.analyzer.Analyzer.analyze(Analyzer.java:83)
	at io.prestosql.sql.analyzer.Analyzer.analyze(Analyzer.java:75)
	at io.prestosql.execution.SqlQueryExecution.analyze(SqlQueryExecution.java:219)
	at io.prestosql.execution.SqlQueryExecution.<init>(SqlQueryExecution.java:178)
	at io.prestosql.execution.SqlQueryExecution.<init>(SqlQueryExecution.java:95)
	at io.prestosql.execution.SqlQueryExecution$SqlQueryExecutionFactory.createQueryExecution(SqlQueryExecution.java:712)
	at io.prestosql.dispatcher.LocalDispatchQueryFactory.lambda$createDispatchQuery$0(LocalDispatchQueryFactory.java:119)
	at io.prestosql.$gen.Presto_null__testversion____20200123_102923_2.call(Unknown Source)
	at com.google.common.util.concurrent.TrustedListenableFutureTask$TrustedFutureInterruptibleTask.runInterruptibly(TrustedListenableFutureTask.java:125)
	at com.google.common.util.concurrent.InterruptibleTask.run(InterruptibleTask.java:57)
	at com.google.common.util.concurrent.TrustedListenableFutureTask.run(TrustedListenableFutureTask.java:78)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
```


Issue discussion:
The issue is that the rewrite is creating a VALUES syntax node with no rows. 

The long-term fix for this is to add first-class support for `SHOW xxx` to the planner instead of relying on syntactic rewrites and create an empty ValuesNode directly. In the short term, we'll have to fix it by synthesizing a `VALUES` with at least one bogus row and a `WHERE false` on top of it. 

Solution patch:
diff --git a/presto-main/src/main/java/io/prestosql/sql/rewrite/ShowQueriesRewrite.java b/presto-main/src/main/java/io/prestosql/sql/rewrite/ShowQueriesRewrite.java
index 356e3e8f002d..e2c79a72ef7e 100644
--- a/presto-main/src/main/java/io/prestosql/sql/rewrite/ShowQueriesRewrite.java
+++ b/presto-main/src/main/java/io/prestosql/sql/rewrite/ShowQueriesRewrite.java
@@ -343,14 +343,17 @@ protected Node visitShowCatalogs(ShowCatalogs node, Void context)
         {
             List<Expression> rows = listCatalogs(session, metadata, accessControl).keySet().stream()
                     .map(name -> row(new StringLiteral(name)))
-                    .collect(toList());
+                    .collect(toImmutableList());
 
             Optional<Expression> predicate = Optional.empty();
-            Optional<String> likePattern = node.getLikePattern();
-            if (likePattern.isPresent()) {
+            if (rows.isEmpty()) {
+                rows = ImmutableList.of(new StringLiteral(""));
+                predicate = Optional.of(BooleanLiteral.FALSE_LITERAL);
+            }
+            else if (node.getLikePattern().isPresent()) {
                 predicate = Optional.of(new LikePredicate(
-                        identifier("Catalog"),
-                        new StringLiteral(likePattern.get()),
+                        identifier("catalog"),
+                        new StringLiteral(node.getLikePattern().get()),
                         node.getEscape().map(StringLiteral::new)));
             }
 


Test patch:
diff --git a/presto-main/src/main/java/io/prestosql/testing/TestingAccessControlManager.java b/presto-main/src/main/java/io/prestosql/testing/TestingAccessControlManager.java
index 91ab9a4551f8..ac894c02480e 100644
--- a/presto-main/src/main/java/io/prestosql/testing/TestingAccessControlManager.java
+++ b/presto-main/src/main/java/io/prestosql/testing/TestingAccessControlManager.java
@@ -32,8 +32,10 @@
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.function.Predicate;
 
 import static com.google.common.base.MoreObjects.toStringHelper;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static io.prestosql.spi.security.AccessDeniedException.denyAddColumn;
 import static io.prestosql.spi.security.AccessDeniedException.denyCommentTable;
 import static io.prestosql.spi.security.AccessDeniedException.denyCreateSchema;
@@ -81,6 +83,7 @@ public class TestingAccessControlManager
         extends AccessControlManager
 {
     private final Set<TestingPrivilege> denyPrivileges = new HashSet<>();
+    private Predicate<String> deniedCatalogs = s -> true;
 
     @Inject
     public TestingAccessControlManager(TransactionManager transactionManager)
@@ -107,6 +110,22 @@ public void deny(TestingPrivilege... deniedPrivileges)
     public void reset()
     {
         denyPrivileges.clear();
+        deniedCatalogs = s -> true;
+    }
+
+    public void denyCatalogs(Predicate<String> deniedCatalogs)
+    {
+        this.deniedCatalogs = this.deniedCatalogs.and(deniedCatalogs);
+    }
+
+    @Override
+    public Set<String> filterCatalogs(Identity identity, Set<String> catalogs)
+    {
+        return super.filterCatalogs(
+                identity,
+                catalogs.stream()
+                        .filter(this.deniedCatalogs)
+                        .collect(toImmutableSet()));
     }
 
     @Override
diff --git a/presto-main/src/test/java/io/prestosql/sql/query/QueryAssertions.java b/presto-main/src/test/java/io/prestosql/sql/query/QueryAssertions.java
index 0933f13b2f28..883dbeb3a8c9 100644
--- a/presto-main/src/test/java/io/prestosql/sql/query/QueryAssertions.java
+++ b/presto-main/src/test/java/io/prestosql/sql/query/QueryAssertions.java
@@ -162,4 +162,20 @@ public void close()
     {
         runner.close();
     }
+
+    public QueryRunner getQueryRunner()
+    {
+        return runner;
+    }
+
+    protected void executeExclusively(Runnable executionBlock)
+    {
+        runner.getExclusiveLock().lock();
+        try {
+            executionBlock.run();
+        }
+        finally {
+            runner.getExclusiveLock().unlock();
+        }
+    }
 }
diff --git a/presto-main/src/test/java/io/prestosql/sql/query/TestShowQueries.java b/presto-main/src/test/java/io/prestosql/sql/query/TestShowQueries.java
index 8a0cbcd5432f..f90cd8c5d293 100644
--- a/presto-main/src/test/java/io/prestosql/sql/query/TestShowQueries.java
+++ b/presto-main/src/test/java/io/prestosql/sql/query/TestShowQueries.java
@@ -81,4 +81,14 @@ public void testShowSessionLikeWithEscape()
                 "SHOW SESSION LIKE '%page$_row$_c%' ESCAPE '$'",
                 "VALUES ('filter_and_project_min_output_page_row_count', cast('256' as VARCHAR(14)), cast('256' as VARCHAR(14)), 'integer', cast('Experimental: Minimum output page row count for filter and project operators' as VARCHAR(118)))");
     }
+
+    @Test
+    public void testListingEmptyCatalogs()
+    {
+        assertions.executeExclusively(() -> {
+            assertions.getQueryRunner().getAccessControl().denyCatalogs(catalog -> false);
+            assertions.assertQueryReturnsEmptyResult("SHOW CATALOGS");
+            assertions.getQueryRunner().getAccessControl().reset();
+        });
+    }
 }