From 4d15575ac31ce339f31b609f1b44cd1f575fb6fc Mon Sep 17 00:00:00 2001 From: "praveenkrishna.d" Date: Mon, 2 Jun 2025 20:43:45 +0530 Subject: [PATCH 1/3] Add parser support for refreshing view --- .../antlr4/io/trino/grammar/sql/SqlBase.g4 | 1 + .../main/java/io/trino/sql/SqlFormatter.java | 10 +++ .../java/io/trino/sql/parser/AstBuilder.java | 9 +++ .../java/io/trino/sql/tree/AstVisitor.java | 5 ++ .../java/io/trino/sql/tree/RefreshView.java | 78 +++++++++++++++++++ .../java/io/trino/sql/TestSqlFormatter.java | 9 +++ .../io/trino/sql/parser/TestSqlParser.java | 10 +++ 7 files changed, 122 insertions(+) create mode 100644 core/trino-parser/src/main/java/io/trino/sql/tree/RefreshView.java diff --git a/core/trino-grammar/src/main/antlr4/io/trino/grammar/sql/SqlBase.g4 b/core/trino-grammar/src/main/antlr4/io/trino/grammar/sql/SqlBase.g4 index 2d9388b39d4d..579be133b6db 100644 --- a/core/trino-grammar/src/main/antlr4/io/trino/grammar/sql/SqlBase.g4 +++ b/core/trino-grammar/src/main/antlr4/io/trino/grammar/sql/SqlBase.g4 @@ -114,6 +114,7 @@ statement SET PROPERTIES propertyAssignments #setMaterializedViewProperties | DROP VIEW (IF EXISTS)? qualifiedName #dropView | ALTER VIEW from=qualifiedName RENAME TO to=qualifiedName #renameView + | ALTER VIEW viewName=qualifiedName REFRESH #refreshView | CALL qualifiedName '(' (callArgument (',' callArgument)*)? ')' #call | CREATE (OR REPLACE)? functionSpecification #createFunction | DROP FUNCTION (IF EXISTS)? functionDeclaration #dropFunction diff --git a/core/trino-parser/src/main/java/io/trino/sql/SqlFormatter.java b/core/trino-parser/src/main/java/io/trino/sql/SqlFormatter.java index c25a20bc67d6..10a98e744915 100644 --- a/core/trino-parser/src/main/java/io/trino/sql/SqlFormatter.java +++ b/core/trino-parser/src/main/java/io/trino/sql/SqlFormatter.java @@ -122,6 +122,7 @@ import io.trino.sql.tree.QueryPeriod; import io.trino.sql.tree.QuerySpecification; import io.trino.sql.tree.RefreshMaterializedView; +import io.trino.sql.tree.RefreshView; import io.trino.sql.tree.Relation; import io.trino.sql.tree.RenameColumn; import io.trino.sql.tree.RenameMaterializedView; @@ -1277,6 +1278,15 @@ protected Void visitRefreshMaterializedView(RefreshMaterializedView node, Intege return null; } + @Override + protected Void visitRefreshView(RefreshView node, Integer indent) + { + builder.append("ALTER VIEW "); + builder.append(formatName(node.getName())); + builder.append(" REFRESH"); + return null; + } + @Override protected Void visitDropMaterializedView(DropMaterializedView node, Integer indent) { diff --git a/core/trino-parser/src/main/java/io/trino/sql/parser/AstBuilder.java b/core/trino-parser/src/main/java/io/trino/sql/parser/AstBuilder.java index ed597614a44e..4fe95c9d6dbe 100644 --- a/core/trino-parser/src/main/java/io/trino/sql/parser/AstBuilder.java +++ b/core/trino-parser/src/main/java/io/trino/sql/parser/AstBuilder.java @@ -223,6 +223,7 @@ import io.trino.sql.tree.QuerySpecification; import io.trino.sql.tree.RangeQuantifier; import io.trino.sql.tree.RefreshMaterializedView; +import io.trino.sql.tree.RefreshView; import io.trino.sql.tree.Relation; import io.trino.sql.tree.RenameColumn; import io.trino.sql.tree.RenameMaterializedView; @@ -638,6 +639,14 @@ public Node visitRefreshMaterializedView(SqlBaseParser.RefreshMaterializedViewCo new Table(getQualifiedName(context.qualifiedName()))); } + @Override + public Node visitRefreshView(SqlBaseParser.RefreshViewContext context) + { + return new RefreshView( + getLocation(context), + getQualifiedName(context.qualifiedName())); + } + @Override public Node visitDropMaterializedView(SqlBaseParser.DropMaterializedViewContext context) { diff --git a/core/trino-parser/src/main/java/io/trino/sql/tree/AstVisitor.java b/core/trino-parser/src/main/java/io/trino/sql/tree/AstVisitor.java index 58a45880434f..0619152ff97d 100644 --- a/core/trino-parser/src/main/java/io/trino/sql/tree/AstVisitor.java +++ b/core/trino-parser/src/main/java/io/trino/sql/tree/AstVisitor.java @@ -732,6 +732,11 @@ protected R visitRefreshMaterializedView(RefreshMaterializedView node, C context return visitStatement(node, context); } + protected R visitRefreshView(RefreshView node, C context) + { + return visitStatement(node, context); + } + protected R visitCall(Call node, C context) { return visitStatement(node, context); diff --git a/core/trino-parser/src/main/java/io/trino/sql/tree/RefreshView.java b/core/trino-parser/src/main/java/io/trino/sql/tree/RefreshView.java new file mode 100644 index 000000000000..8706ebd49036 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/tree/RefreshView.java @@ -0,0 +1,78 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.tree; + +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Objects; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static java.util.Objects.requireNonNull; + +public final class RefreshView + extends Statement +{ + private final QualifiedName name; + + public RefreshView(NodeLocation location, QualifiedName name) + { + super(location); + this.name = requireNonNull(name, "name is null"); + } + + public QualifiedName getName() + { + return name; + } + + @Override + public R accept(AstVisitor visitor, C context) + { + return visitor.visitRefreshView(this, context); + } + + @Override + public List getChildren() + { + return ImmutableList.of(); + } + + @Override + public int hashCode() + { + return Objects.hash(name); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + RefreshView o = (RefreshView) obj; + return Objects.equals(name, o.name); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("name", name) + .toString(); + } +} diff --git a/core/trino-parser/src/test/java/io/trino/sql/TestSqlFormatter.java b/core/trino-parser/src/test/java/io/trino/sql/TestSqlFormatter.java index 12ab5e4dbd71..4853817b00e2 100644 --- a/core/trino-parser/src/test/java/io/trino/sql/TestSqlFormatter.java +++ b/core/trino-parser/src/test/java/io/trino/sql/TestSqlFormatter.java @@ -35,6 +35,7 @@ import io.trino.sql.tree.Property; import io.trino.sql.tree.QualifiedName; import io.trino.sql.tree.Query; +import io.trino.sql.tree.RefreshView; import io.trino.sql.tree.ShowBranches; import io.trino.sql.tree.ShowCatalogs; import io.trino.sql.tree.ShowColumns; @@ -560,6 +561,14 @@ public void testCommentOnColumn() .isEqualTo("COMMENT ON COLUMN test.a IS '攻殻機動隊'"); } + @Test + public void testRefreshView() + { + assertThat(formatSql( + new RefreshView(new NodeLocation(1, 1), QualifiedName.of("catalog", "schema", "view")))) + .isEqualTo("ALTER VIEW catalog.schema.view REFRESH"); + } + @Test public void testExecuteImmediate() { diff --git a/core/trino-parser/src/test/java/io/trino/sql/parser/TestSqlParser.java b/core/trino-parser/src/test/java/io/trino/sql/parser/TestSqlParser.java index abae8eaffbda..d6cc85a4b012 100644 --- a/core/trino-parser/src/test/java/io/trino/sql/parser/TestSqlParser.java +++ b/core/trino-parser/src/test/java/io/trino/sql/parser/TestSqlParser.java @@ -166,6 +166,7 @@ import io.trino.sql.tree.QuerySpecification; import io.trino.sql.tree.RangeQuantifier; import io.trino.sql.tree.RefreshMaterializedView; +import io.trino.sql.tree.RefreshView; import io.trino.sql.tree.Relation; import io.trino.sql.tree.RenameColumn; import io.trino.sql.tree.RenameMaterializedView; @@ -3700,6 +3701,15 @@ public void testRenameView() QualifiedName.of(ImmutableList.of(new Identifier(location(1, 24), "b", false))))); } + @Test + public void testRefreshView() + { + assertThat(statement("ALTER VIEW a REFRESH")) + .isEqualTo(new RefreshView( + location(1, 1), + QualifiedName.of(ImmutableList.of(new Identifier(location(1, 12), "a", false))))); + } + @Test public void testAlterViewSetAuthorization() { From 4cd68c13645227c14d8e8b57ceb2ce9a54a7753c Mon Sep 17 00:00:00 2001 From: "praveenkrishna.d" Date: Mon, 2 Jun 2025 22:14:26 +0530 Subject: [PATCH 2/3] Add engine support for refreshing view --- .../io/trino/execution/RefreshViewTask.java | 148 ++++++++ .../main/java/io/trino/metadata/Metadata.java | 5 + .../io/trino/metadata/MetadataManager.java | 9 + .../java/io/trino/security/AccessControl.java | 7 + .../trino/security/AccessControlManager.java | 13 + .../trino/security/AllowAllAccessControl.java | 3 + .../trino/security/DenyAllAccessControl.java | 7 + .../security/ForwardingAccessControl.java | 6 + .../InjectedConnectorAccessControl.java | 7 + .../server/QueryExecutionFactoryModule.java | 3 + .../trino/sql/analyzer/AnalyzerFactory.java | 21 ++ .../trino/sql/analyzer/StatementAnalyzer.java | 7 + .../trino/tracing/TracingAccessControl.java | 9 + .../tracing/TracingConnectorMetadata.java | 9 + .../io/trino/tracing/TracingMetadata.java | 9 + .../java/io/trino/util/StatementUtils.java | 3 + .../execution/BaseDataDefinitionTaskTest.java | 8 + .../trino/execution/TestRefreshViewTask.java | 344 ++++++++++++++++++ .../trino/metadata/AbstractMockMetadata.java | 6 + .../spi/connector/ConnectorAccessControl.java | 11 + .../spi/connector/ConnectorMetadata.java | 8 + .../spi/security/AccessDeniedException.java | 10 + .../spi/security/SystemAccessControl.java | 11 + ...ClassLoaderSafeConnectorAccessControl.java | 8 + .../ClassLoaderSafeConnectorMetadata.java | 8 + .../base/security/AllowAllAccessControl.java | 3 + .../security/AllowAllSystemAccessControl.java | 3 + .../base/security/FileBasedAccessControl.java | 10 + .../FileBasedSystemAccessControl.java | 9 + .../ForwardingConnectorAccessControl.java | 6 + .../ForwardingSystemAccessControl.java | 6 + .../security/SqlStandardAccessControl.java | 9 + .../plugin/lakehouse/LakehouseMetadata.java | 8 + .../java/io/trino/verifier/VerifyCommand.java | 4 + 34 files changed, 738 insertions(+) create mode 100644 core/trino-main/src/main/java/io/trino/execution/RefreshViewTask.java create mode 100644 core/trino-main/src/test/java/io/trino/execution/TestRefreshViewTask.java diff --git a/core/trino-main/src/main/java/io/trino/execution/RefreshViewTask.java b/core/trino-main/src/main/java/io/trino/execution/RefreshViewTask.java new file mode 100644 index 000000000000..e83e4e329830 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/execution/RefreshViewTask.java @@ -0,0 +1,148 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.execution; + +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.inject.Inject; +import io.trino.Session; +import io.trino.execution.warnings.WarningCollector; +import io.trino.metadata.Metadata; +import io.trino.metadata.QualifiedObjectName; +import io.trino.metadata.ViewColumn; +import io.trino.metadata.ViewDefinition; +import io.trino.security.AccessControl; +import io.trino.security.ViewAccessControl; +import io.trino.spi.security.GroupProvider; +import io.trino.spi.security.Identity; +import io.trino.sql.PlannerContext; +import io.trino.sql.analyzer.Analysis; +import io.trino.sql.analyzer.AnalyzerFactory; +import io.trino.sql.parser.SqlParser; +import io.trino.sql.tree.Expression; +import io.trino.sql.tree.RefreshView; +import io.trino.sql.tree.Statement; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static com.google.common.util.concurrent.Futures.immediateVoidFuture; +import static io.trino.metadata.MetadataUtil.createQualifiedObjectName; +import static io.trino.spi.StandardErrorCode.TABLE_NOT_FOUND; +import static io.trino.sql.analyzer.SemanticExceptions.semanticException; +import static java.util.Objects.requireNonNull; + +public class RefreshViewTask + implements DataDefinitionTask +{ + private final PlannerContext plannerContext; + private final AccessControl accessControl; + private final GroupProvider groupProvider; + private final SqlParser sqlParser; + private final AnalyzerFactory analyzerFactory; + + @Inject + public RefreshViewTask( + PlannerContext plannerContext, + AccessControl accessControl, + GroupProvider groupProvider, + SqlParser sqlParser, + AnalyzerFactory analyzerFactory) + { + this.plannerContext = requireNonNull(plannerContext, "plannerContext is null"); + this.accessControl = requireNonNull(accessControl, "accessControl is null"); + this.groupProvider = requireNonNull(groupProvider, "groupProvider is null"); + this.sqlParser = requireNonNull(sqlParser, "sqlParser is null"); + this.analyzerFactory = requireNonNull(analyzerFactory, "analyzerFactory is null"); + } + + @Override + public String getName() + { + return "REFRESH VIEW"; + } + + @Override + public ListenableFuture execute( + RefreshView refreshView, + QueryStateMachine stateMachine, + List parameters, + WarningCollector warningCollector) + { + Metadata metadata = plannerContext.getMetadata(); + Session session = stateMachine.getSession(); + QualifiedObjectName viewName = createQualifiedObjectName(session, refreshView, refreshView.getName()); + + ViewDefinition viewDefinition = metadata.getView(session, viewName) + .orElseThrow(() -> semanticException(TABLE_NOT_FOUND, refreshView, "View '%s' not found", viewName)); + + accessControl.checkCanRefreshView(session.toSecurityContext(), viewName); + + Identity identity = session.getIdentity(); + AccessControl viewAccessControl = accessControl; + + if (!viewDefinition.isRunAsInvoker()) { + checkArgument(viewDefinition.getRunAsIdentity().isPresent(), "View owner detail is missing"); + Identity owner = viewDefinition.getRunAsIdentity().get(); + identity = Identity.from(owner) + .withGroups(groupProvider.getGroups(owner.getUser())) + .build(); + // View owner does not need GRANT OPTION to grant access themselves + if (!owner.getUser().equals(session.getIdentity().getUser())) { + viewAccessControl = new ViewAccessControl(accessControl); + } + } + + Session viewSession = session.createViewSession(viewDefinition.getCatalog(), viewDefinition.getSchema(), identity, viewDefinition.getPath()); + + Statement viewDefinitionSql = sqlParser.createStatement(viewDefinition.getOriginalSql()); + + Analysis analysis = analyzerFactory.createAnalyzer( + viewSession, + parameters, + viewAccessControl, + ImmutableMap.of(), + stateMachine.getWarningCollector(), + stateMachine.getPlanOptimizersStatsCollector()) + .analyze(viewDefinitionSql); + + Map columnComments = + viewDefinition.getColumns() + .stream() + .filter(viewColumn -> viewColumn.comment().isPresent()) + .collect(toImmutableMap(ViewColumn::name, viewColumn -> viewColumn.comment().get())); + + List columns = analysis.getOutputDescriptor(viewDefinitionSql) + .getVisibleFields().stream() + .map(field -> new ViewColumn(field.getName().get(), field.getType().getTypeId(), Optional.ofNullable(columnComments.get(field.getName().get())))) + .collect(toImmutableList()); + + ViewDefinition viewDefinitionWithNewColumns = new ViewDefinition( + viewDefinition.getOriginalSql(), + viewDefinition.getCatalog(), + viewDefinition.getSchema(), + columns, + viewDefinition.getComment(), + viewDefinition.getRunAsIdentity(), + viewDefinition.getPath()); + + metadata.refreshView(session, viewName, viewDefinitionWithNewColumns); + + return immediateVoidFuture(); + } +} diff --git a/core/trino-main/src/main/java/io/trino/metadata/Metadata.java b/core/trino-main/src/main/java/io/trino/metadata/Metadata.java index 54d4c6c50374..5929e0b0ca9a 100644 --- a/core/trino-main/src/main/java/io/trino/metadata/Metadata.java +++ b/core/trino-main/src/main/java/io/trino/metadata/Metadata.java @@ -513,6 +513,11 @@ Optional finishRefreshMaterializedView( */ void renameView(Session session, QualifiedObjectName existingViewName, QualifiedObjectName newViewName); + /** + * Refreshes the view definition. + */ + void refreshView(Session session, QualifiedObjectName viewName, ViewDefinition definition); + /** * Drops the specified view. */ diff --git a/core/trino-main/src/main/java/io/trino/metadata/MetadataManager.java b/core/trino-main/src/main/java/io/trino/metadata/MetadataManager.java index 1baf62fd6afe..6a1623164d42 100644 --- a/core/trino-main/src/main/java/io/trino/metadata/MetadataManager.java +++ b/core/trino-main/src/main/java/io/trino/metadata/MetadataManager.java @@ -1585,6 +1585,15 @@ public void renameView(Session session, QualifiedObjectName source, QualifiedObj } } + @Override + public void refreshView(Session session, QualifiedObjectName viewName, ViewDefinition definition) + { + CatalogMetadata catalogMetadata = getCatalogMetadataForWrite(session, viewName.catalogName()); + CatalogHandle catalogHandle = catalogMetadata.getCatalogHandle(); + ConnectorMetadata metadata = catalogMetadata.getMetadata(session); + metadata.refreshView(session.toConnectorSession(catalogHandle), viewName.asSchemaTableName(), definition.toConnectorViewDefinition()); + } + @Override public void dropView(Session session, QualifiedObjectName viewName) { diff --git a/core/trino-main/src/main/java/io/trino/security/AccessControl.java b/core/trino-main/src/main/java/io/trino/security/AccessControl.java index 8afaf1088925..7f26ea10adbd 100644 --- a/core/trino-main/src/main/java/io/trino/security/AccessControl.java +++ b/core/trino-main/src/main/java/io/trino/security/AccessControl.java @@ -322,6 +322,13 @@ public interface AccessControl */ void checkCanRenameView(SecurityContext context, QualifiedObjectName viewName, QualifiedObjectName newViewName); + /** + * Check if identity is allowed to refresh the specified view. + * + * @throws AccessDeniedException if not allowed + */ + void checkCanRefreshView(SecurityContext context, QualifiedObjectName viewName); + /** * Check if identity is allowed to drop the specified view. * diff --git a/core/trino-main/src/main/java/io/trino/security/AccessControlManager.java b/core/trino-main/src/main/java/io/trino/security/AccessControlManager.java index a785c8cff39d..bf464b4076ed 100644 --- a/core/trino-main/src/main/java/io/trino/security/AccessControlManager.java +++ b/core/trino-main/src/main/java/io/trino/security/AccessControlManager.java @@ -797,6 +797,19 @@ public void checkCanRenameView(SecurityContext securityContext, QualifiedObjectN catalogAuthorizationCheck(viewName.catalogName(), securityContext, (control, context) -> control.checkCanRenameView(context, viewName.asSchemaTableName(), newViewName.asSchemaTableName())); } + @Override + public void checkCanRefreshView(SecurityContext securityContext, QualifiedObjectName viewName) + { + requireNonNull(securityContext, "securityContext is null"); + requireNonNull(viewName, "viewName is null"); + + checkCanAccessCatalog(securityContext, viewName.catalogName()); + + systemAuthorizationCheck(control -> control.checkCanRefreshView(securityContext.toSystemSecurityContext(), viewName.asCatalogSchemaTableName())); + + catalogAuthorizationCheck(viewName.catalogName(), securityContext, (control, context) -> control.checkCanRefreshView(context, viewName.asSchemaTableName())); + } + @Override public void checkCanDropView(SecurityContext securityContext, QualifiedObjectName viewName) { diff --git a/core/trino-main/src/main/java/io/trino/security/AllowAllAccessControl.java b/core/trino-main/src/main/java/io/trino/security/AllowAllAccessControl.java index 97e1b8beabd9..5d983a1e9a33 100644 --- a/core/trino-main/src/main/java/io/trino/security/AllowAllAccessControl.java +++ b/core/trino-main/src/main/java/io/trino/security/AllowAllAccessControl.java @@ -166,6 +166,9 @@ public void checkCanCreateView(SecurityContext context, QualifiedObjectName view @Override public void checkCanRenameView(SecurityContext context, QualifiedObjectName viewName, QualifiedObjectName newViewName) {} + @Override + public void checkCanRefreshView(SecurityContext context, QualifiedObjectName viewName) {} + @Override public void checkCanDropView(SecurityContext context, QualifiedObjectName viewName) {} diff --git a/core/trino-main/src/main/java/io/trino/security/DenyAllAccessControl.java b/core/trino-main/src/main/java/io/trino/security/DenyAllAccessControl.java index ebe24cee6d83..24c5de590485 100644 --- a/core/trino-main/src/main/java/io/trino/security/DenyAllAccessControl.java +++ b/core/trino-main/src/main/java/io/trino/security/DenyAllAccessControl.java @@ -74,6 +74,7 @@ import static io.trino.spi.security.AccessDeniedException.denyKillQuery; import static io.trino.spi.security.AccessDeniedException.denyReadSystemInformationAccess; import static io.trino.spi.security.AccessDeniedException.denyRefreshMaterializedView; +import static io.trino.spi.security.AccessDeniedException.denyRefreshView; import static io.trino.spi.security.AccessDeniedException.denyRenameColumn; import static io.trino.spi.security.AccessDeniedException.denyRenameMaterializedView; import static io.trino.spi.security.AccessDeniedException.denyRenameSchema; @@ -344,6 +345,12 @@ public void checkCanRenameView(SecurityContext context, QualifiedObjectName view denyRenameView(viewName.toString(), newViewName.toString()); } + @Override + public void checkCanRefreshView(SecurityContext context, QualifiedObjectName viewName) + { + denyRefreshView(viewName.toString()); + } + @Override public void checkCanDropView(SecurityContext context, QualifiedObjectName viewName) { diff --git a/core/trino-main/src/main/java/io/trino/security/ForwardingAccessControl.java b/core/trino-main/src/main/java/io/trino/security/ForwardingAccessControl.java index 2788636588b5..b731b9da97a0 100644 --- a/core/trino-main/src/main/java/io/trino/security/ForwardingAccessControl.java +++ b/core/trino-main/src/main/java/io/trino/security/ForwardingAccessControl.java @@ -290,6 +290,12 @@ public void checkCanRenameView(SecurityContext context, QualifiedObjectName view delegate().checkCanRenameView(context, viewName, newViewName); } + @Override + public void checkCanRefreshView(SecurityContext context, QualifiedObjectName viewName) + { + delegate().checkCanRefreshView(context, viewName); + } + @Override public void checkCanDropView(SecurityContext context, QualifiedObjectName viewName) { diff --git a/core/trino-main/src/main/java/io/trino/security/InjectedConnectorAccessControl.java b/core/trino-main/src/main/java/io/trino/security/InjectedConnectorAccessControl.java index d417b62b06ca..034938bd27ff 100644 --- a/core/trino-main/src/main/java/io/trino/security/InjectedConnectorAccessControl.java +++ b/core/trino-main/src/main/java/io/trino/security/InjectedConnectorAccessControl.java @@ -279,6 +279,13 @@ public void checkCanRenameView(ConnectorSecurityContext context, SchemaTableName accessControl.checkCanRenameView(securityContext, getQualifiedObjectName(viewName), getQualifiedObjectName(viewName)); } + @Override + public void checkCanRefreshView(ConnectorSecurityContext context, SchemaTableName viewName) + { + checkArgument(context == null, "context must be null"); + accessControl.checkCanRefreshView(securityContext, getQualifiedObjectName(viewName)); + } + @Override public void checkCanDropView(ConnectorSecurityContext context, SchemaTableName viewName) { diff --git a/core/trino-main/src/main/java/io/trino/server/QueryExecutionFactoryModule.java b/core/trino-main/src/main/java/io/trino/server/QueryExecutionFactoryModule.java index 372c896e52b8..4db7cf53f037 100644 --- a/core/trino-main/src/main/java/io/trino/server/QueryExecutionFactoryModule.java +++ b/core/trino-main/src/main/java/io/trino/server/QueryExecutionFactoryModule.java @@ -49,6 +49,7 @@ import io.trino.execution.GrantTask; import io.trino.execution.PrepareTask; import io.trino.execution.QueryExecution.QueryExecutionFactory; +import io.trino.execution.RefreshViewTask; import io.trino.execution.RenameColumnTask; import io.trino.execution.RenameMaterializedViewTask; import io.trino.execution.RenameSchemaTask; @@ -99,6 +100,7 @@ import io.trino.sql.tree.Grant; import io.trino.sql.tree.GrantRoles; import io.trino.sql.tree.Prepare; +import io.trino.sql.tree.RefreshView; import io.trino.sql.tree.RenameColumn; import io.trino.sql.tree.RenameMaterializedView; import io.trino.sql.tree.RenameSchema; @@ -170,6 +172,7 @@ public void configure(Binder binder) bindDataDefinitionTask(binder, executionBinder, Grant.class, GrantTask.class); bindDataDefinitionTask(binder, executionBinder, GrantRoles.class, GrantRolesTask.class); bindDataDefinitionTask(binder, executionBinder, Prepare.class, PrepareTask.class); + bindDataDefinitionTask(binder, executionBinder, RefreshView.class, RefreshViewTask.class); bindDataDefinitionTask(binder, executionBinder, RenameColumn.class, RenameColumnTask.class); bindDataDefinitionTask(binder, executionBinder, RenameMaterializedView.class, RenameMaterializedViewTask.class); bindDataDefinitionTask(binder, executionBinder, RenameSchema.class, RenameSchemaTask.class); diff --git a/core/trino-main/src/main/java/io/trino/sql/analyzer/AnalyzerFactory.java b/core/trino-main/src/main/java/io/trino/sql/analyzer/AnalyzerFactory.java index 424bac0dcd92..21f180712116 100644 --- a/core/trino-main/src/main/java/io/trino/sql/analyzer/AnalyzerFactory.java +++ b/core/trino-main/src/main/java/io/trino/sql/analyzer/AnalyzerFactory.java @@ -18,6 +18,7 @@ import io.trino.Session; import io.trino.execution.querystats.PlanOptimizersStatsCollector; import io.trino.execution.warnings.WarningCollector; +import io.trino.security.AccessControl; import io.trino.sql.rewrite.StatementRewrite; import io.trino.sql.tree.Expression; import io.trino.sql.tree.NodeRef; @@ -60,4 +61,24 @@ public Analyzer createAnalyzer( tracer, statementRewrite); } + + public Analyzer createAnalyzer( + Session session, + List parameters, + AccessControl accessControl, + Map, Expression> parameterLookup, + WarningCollector warningCollector, + PlanOptimizersStatsCollector planOptimizersStatsCollector) + { + return new Analyzer( + session, + this, + statementAnalyzerFactory.withSpecializedAccessControl(accessControl), + parameters, + parameterLookup, + warningCollector, + planOptimizersStatsCollector, + tracer, + statementRewrite); + } } diff --git a/core/trino-main/src/main/java/io/trino/sql/analyzer/StatementAnalyzer.java b/core/trino-main/src/main/java/io/trino/sql/analyzer/StatementAnalyzer.java index 20a99778d5cc..edd1b78c997e 100644 --- a/core/trino-main/src/main/java/io/trino/sql/analyzer/StatementAnalyzer.java +++ b/core/trino-main/src/main/java/io/trino/sql/analyzer/StatementAnalyzer.java @@ -215,6 +215,7 @@ import io.trino.sql.tree.QueryPeriod; import io.trino.sql.tree.QuerySpecification; import io.trino.sql.tree.RefreshMaterializedView; +import io.trino.sql.tree.RefreshView; import io.trino.sql.tree.Relation; import io.trino.sql.tree.RenameColumn; import io.trino.sql.tree.RenameMaterializedView; @@ -689,6 +690,12 @@ protected Scope visitInsert(Insert insert, Optional scope) return createAndAssignScope(insert, scope, Field.newUnqualified("rows", BIGINT)); } + @Override + protected Scope visitRefreshView(RefreshView node, Optional scope) + { + return createAndAssignScope(node, scope); + } + @Override protected Scope visitRefreshMaterializedView(RefreshMaterializedView refreshMaterializedView, Optional scope) { diff --git a/core/trino-main/src/main/java/io/trino/tracing/TracingAccessControl.java b/core/trino-main/src/main/java/io/trino/tracing/TracingAccessControl.java index c1c50e566bab..308c9f5e0856 100644 --- a/core/trino-main/src/main/java/io/trino/tracing/TracingAccessControl.java +++ b/core/trino-main/src/main/java/io/trino/tracing/TracingAccessControl.java @@ -413,6 +413,15 @@ public void checkCanRenameView(SecurityContext context, QualifiedObjectName view } } + @Override + public void checkCanRefreshView(SecurityContext context, QualifiedObjectName viewName) + { + Span span = startSpan("checkCanRefreshView"); + try (var _ = scopedSpan(span)) { + delegate.checkCanRefreshView(context, viewName); + } + } + @Override public void checkCanDropView(SecurityContext context, QualifiedObjectName viewName) { diff --git a/core/trino-main/src/main/java/io/trino/tracing/TracingConnectorMetadata.java b/core/trino-main/src/main/java/io/trino/tracing/TracingConnectorMetadata.java index 712e8a9278fa..0335b96375b8 100644 --- a/core/trino-main/src/main/java/io/trino/tracing/TracingConnectorMetadata.java +++ b/core/trino-main/src/main/java/io/trino/tracing/TracingConnectorMetadata.java @@ -798,6 +798,15 @@ public void setViewAuthorization(ConnectorSession session, SchemaTableName viewN } } + @Override + public void refreshView(ConnectorSession session, SchemaTableName viewName, ConnectorViewDefinition definition) + { + Span span = startSpan("refreshView", viewName); + try (var _ = scopedSpan(span)) { + delegate.refreshView(session, viewName, definition); + } + } + @Override public void dropView(ConnectorSession session, SchemaTableName viewName) { diff --git a/core/trino-main/src/main/java/io/trino/tracing/TracingMetadata.java b/core/trino-main/src/main/java/io/trino/tracing/TracingMetadata.java index 1aca5b9e8cf2..f6ab61124f74 100644 --- a/core/trino-main/src/main/java/io/trino/tracing/TracingMetadata.java +++ b/core/trino-main/src/main/java/io/trino/tracing/TracingMetadata.java @@ -930,6 +930,15 @@ public void renameView(Session session, QualifiedObjectName existingViewName, Qu } } + @Override + public void refreshView(Session session, QualifiedObjectName viewName, ViewDefinition viewDefinition) + { + Span span = startSpan("refreshView", viewName); + try (var _ = scopedSpan(span)) { + delegate.refreshView(session, viewName, viewDefinition); + } + } + @Override public void dropView(Session session, QualifiedObjectName viewName) { diff --git a/core/trino-main/src/main/java/io/trino/util/StatementUtils.java b/core/trino-main/src/main/java/io/trino/util/StatementUtils.java index b63d181ba018..ff4256ce0b96 100644 --- a/core/trino-main/src/main/java/io/trino/util/StatementUtils.java +++ b/core/trino-main/src/main/java/io/trino/util/StatementUtils.java @@ -44,6 +44,7 @@ import io.trino.execution.GrantRolesTask; import io.trino.execution.GrantTask; import io.trino.execution.PrepareTask; +import io.trino.execution.RefreshViewTask; import io.trino.execution.RenameColumnTask; import io.trino.execution.RenameMaterializedViewTask; import io.trino.execution.RenameSchemaTask; @@ -105,6 +106,7 @@ import io.trino.sql.tree.Prepare; import io.trino.sql.tree.Query; import io.trino.sql.tree.RefreshMaterializedView; +import io.trino.sql.tree.RefreshView; import io.trino.sql.tree.RenameColumn; import io.trino.sql.tree.RenameMaterializedView; import io.trino.sql.tree.RenameSchema; @@ -229,6 +231,7 @@ private StatementUtils() {} .add(dataDefinitionStatement(Grant.class, GrantTask.class)) .add(dataDefinitionStatement(GrantRoles.class, GrantRolesTask.class)) .add(dataDefinitionStatement(Prepare.class, PrepareTask.class)) + .add(dataDefinitionStatement(RefreshView.class, RefreshViewTask.class)) .add(dataDefinitionStatement(RenameColumn.class, RenameColumnTask.class)) .add(dataDefinitionStatement(RenameMaterializedView.class, RenameMaterializedViewTask.class)) .add(dataDefinitionStatement(RenameSchema.class, RenameSchemaTask.class)) diff --git a/core/trino-main/src/test/java/io/trino/execution/BaseDataDefinitionTaskTest.java b/core/trino-main/src/test/java/io/trino/execution/BaseDataDefinitionTaskTest.java index b5d26e7e5391..fe2f795b6854 100644 --- a/core/trino-main/src/test/java/io/trino/execution/BaseDataDefinitionTaskTest.java +++ b/core/trino-main/src/test/java/io/trino/execution/BaseDataDefinitionTaskTest.java @@ -91,6 +91,7 @@ import static io.trino.spi.StandardErrorCode.DIVISION_BY_ZERO; import static io.trino.spi.connector.SaveMode.IGNORE; import static io.trino.spi.connector.SaveMode.REPLACE; +import static io.trino.spi.security.PrincipalType.ROLE; import static io.trino.spi.session.PropertyMetadata.longProperty; import static io.trino.spi.session.PropertyMetadata.stringProperty; import static io.trino.spi.type.BigintType.BIGINT; @@ -146,6 +147,7 @@ MATERIALIZED_VIEW_PROPERTY_1_NAME, longProperty(MATERIALIZED_VIEW_PROPERTY_1_NAM MATERIALIZED_VIEW_PROPERTY_2_NAME, stringProperty(MATERIALIZED_VIEW_PROPERTY_2_NAME, "property 2", MATERIALIZED_VIEW_PROPERTY_2_DEFAULT_VALUE, false)); materializedViewPropertyManager = new MaterializedViewPropertyManager(CatalogServiceProvider.singleton(TEST_CATALOG_HANDLE, properties)); queryStateMachine = stateMachine(transactionManager, createTestMetadataManager(), new AllowAllAccessControl(), testSession); + metadata.createSchema(testSession, new CatalogSchemaName(TEST_CATALOG_NAME, SCHEMA), ImmutableMap.of(), new TrinoPrincipal(ROLE, "role")); } @AfterEach @@ -595,6 +597,12 @@ public void renameView(Session session, QualifiedObjectName source, QualifiedObj views.remove(oldViewName); } + @Override + public void refreshView(Session session, QualifiedObjectName viewName, ViewDefinition viewDefinition) + { + views.replace(viewName.asSchemaTableName(), viewDefinition); + } + @Override public void setTableComment(Session session, TableHandle tableHandle, Optional comment) { diff --git a/core/trino-main/src/test/java/io/trino/execution/TestRefreshViewTask.java b/core/trino-main/src/test/java/io/trino/execution/TestRefreshViewTask.java new file mode 100644 index 000000000000..f8303cf5e661 --- /dev/null +++ b/core/trino-main/src/test/java/io/trino/execution/TestRefreshViewTask.java @@ -0,0 +1,344 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.execution; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.ListenableFuture; +import io.trino.connector.CatalogServiceProvider; +import io.trino.execution.warnings.WarningCollector; +import io.trino.metadata.AnalyzePropertyManager; +import io.trino.metadata.QualifiedObjectName; +import io.trino.metadata.TableHandle; +import io.trino.metadata.TablePropertyManager; +import io.trino.metadata.ViewColumn; +import io.trino.metadata.ViewDefinition; +import io.trino.security.AccessControl; +import io.trino.security.AllowAllAccessControl; +import io.trino.security.SecurityContext; +import io.trino.spi.TrinoException; +import io.trino.spi.connector.CatalogSchemaTableName; +import io.trino.spi.connector.ColumnMetadata; +import io.trino.spi.connector.ConnectorTableMetadata; +import io.trino.spi.security.Identity; +import io.trino.spi.type.TypeId; +import io.trino.sql.analyzer.AnalyzerFactory; +import io.trino.sql.parser.SqlParser; +import io.trino.sql.rewrite.StatementRewrite; +import io.trino.sql.tree.NodeLocation; +import io.trino.sql.tree.QualifiedName; +import io.trino.sql.tree.RefreshView; +import io.trino.testing.TestingGroupProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Optional; +import java.util.Set; + +import static io.airlift.concurrent.MoreFutures.getFutureValue; +import static io.trino.spi.connector.SaveMode.FAIL; +import static io.trino.spi.security.AccessDeniedException.denySelectColumns; +import static io.trino.spi.type.BigintType.BIGINT; +import static io.trino.spi.type.VarcharType.VARCHAR; +import static io.trino.sql.analyzer.StatementAnalyzerFactory.createTestingStatementAnalyzerFactory; +import static io.trino.testing.TestingHandles.TEST_CATALOG_NAME; +import static java.util.Objects.requireNonNull; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +final class TestRefreshViewTask + extends BaseDataDefinitionTaskTest +{ + private SqlParser parser; + private AnalyzerFactory analyzerFactory; + + @Override + @BeforeEach + public void setUp() + { + super.setUp(); + parser = new SqlParser(); + analyzerFactory = new AnalyzerFactory( + createTestingStatementAnalyzerFactory( + plannerContext, + new AllowAllAccessControl(), + new TablePropertyManager(CatalogServiceProvider.fail()), + new AnalyzePropertyManager(CatalogServiceProvider.fail())), + new StatementRewrite(ImmutableSet.of()), + plannerContext.getTracer()); + } + + @Test + void testAddNewColumnsOnTableWhenRefreshingExistingView() + { + QualifiedObjectName tableName = qualifiedObjectName("existing_table"); + metadata.createTable(testSession, TEST_CATALOG_NAME, someTable(tableName), FAIL); + metadata.createView( + testSession, + qualifiedObjectName("existing_view"), + new ViewDefinition( + "SELECT * FROM test_catalog.schema.existing_table", + Optional.empty(), + Optional.empty(), + ImmutableList.of(new ViewColumn("test", TypeId.of("bigint"), Optional.empty())), + Optional.empty(), + Optional.empty(), + ImmutableList.of()), + ImmutableMap.of(), + false); + + metadata.addColumn( + testSession, + metadata.getTableHandle(testSession, tableName).orElseThrow(), + new CatalogSchemaTableName(TEST_CATALOG_NAME, SCHEMA, "existing_table"), + new ColumnMetadata("new_col", BIGINT), + new io.trino.spi.connector.ColumnPosition.Last()); + + getFutureValue(executeRefreshView(asQualifiedName(qualifiedObjectName("existing_view")))); + assertThat(metadata.getView(testSession, qualifiedObjectName("existing_view")).orElseThrow().getColumns()) + .containsExactly( + new ViewColumn("test", TypeId.of("bigint"), Optional.empty()), + new ViewColumn("new_col", TypeId.of("bigint"), Optional.empty())); + } + + @Test + void testDropColumnsOnTableWhenRefreshingExistingView() + { + QualifiedObjectName tableName = qualifiedObjectName("existing_table"); + metadata.createTable( + testSession, + TEST_CATALOG_NAME, + new ConnectorTableMetadata( + tableName.asSchemaTableName(), + ImmutableList.of( + new ColumnMetadata("test", BIGINT), + new ColumnMetadata("column_to_be_dropped", BIGINT))), + FAIL); + metadata.createView( + testSession, + qualifiedObjectName("existing_view"), + new ViewDefinition( + "SELECT * FROM test_catalog.schema.existing_table", + Optional.empty(), + Optional.empty(), + ImmutableList.of( + new ViewColumn("test", TypeId.of("bigint"), Optional.empty()), + new ViewColumn("column_to_be_dropped", TypeId.of("bigint"), Optional.empty())), + Optional.empty(), + Optional.empty(), + ImmutableList.of()), + ImmutableMap.of(), + false); + + TableHandle tableHandle = metadata.getTableHandle(testSession, tableName).orElseThrow(); + metadata.dropColumn( + testSession, + tableHandle, + new CatalogSchemaTableName(TEST_CATALOG_NAME, SCHEMA, "existing_table"), + metadata.getColumnHandles(testSession, tableHandle).get("column_to_be_dropped")); + + getFutureValue(executeRefreshView(asQualifiedName(qualifiedObjectName("existing_view")))); + assertThat(metadata.getView(testSession, qualifiedObjectName("existing_view")).orElseThrow().getColumns()) + .containsExactly(new ViewColumn("test", TypeId.of("bigint"), Optional.empty())); + } + + @Test + void testRenameColumnsOnTableWhenRefreshingExistingView() + { + QualifiedObjectName tableName = qualifiedObjectName("existing_table"); + metadata.createTable(testSession, TEST_CATALOG_NAME, someTable(tableName), FAIL); + metadata.createView( + testSession, + qualifiedObjectName("existing_view"), + new ViewDefinition( + "SELECT * FROM test_catalog.schema.existing_table", + Optional.empty(), + Optional.empty(), + ImmutableList.of(new ViewColumn("test", TypeId.of("bigint"), Optional.empty())), + Optional.empty(), + Optional.empty(), + ImmutableList.of()), + ImmutableMap.of(), + false); + + TableHandle tableHandle = metadata.getTableHandle(testSession, tableName).orElseThrow(); + metadata.renameColumn( + testSession, + tableHandle, + new CatalogSchemaTableName(TEST_CATALOG_NAME, SCHEMA, "existing_table"), + metadata.getColumnHandles(testSession, tableHandle).get("test"), + "renamed_column"); + + getFutureValue(executeRefreshView(asQualifiedName(qualifiedObjectName("existing_view")))); + assertThat(metadata.getView(testSession, qualifiedObjectName("existing_view")).orElseThrow().getColumns()) + .containsExactly(new ViewColumn("renamed_column", TypeId.of("bigint"), Optional.empty())); + } + + @Test + void testColumnTypeChangeOnTableWhenRefreshingExistingView() + { + QualifiedObjectName tableName = qualifiedObjectName("existing_table"); + metadata.createTable(testSession, TEST_CATALOG_NAME, someTable(tableName), FAIL); + metadata.createView( + testSession, + qualifiedObjectName("existing_view"), + new ViewDefinition( + "SELECT * FROM test_catalog.schema.existing_table", + Optional.empty(), + Optional.empty(), + ImmutableList.of(new ViewColumn("test", TypeId.of("bigint"), Optional.empty())), + Optional.empty(), + Optional.empty(), + ImmutableList.of()), + ImmutableMap.of(), + false); + + TableHandle tableHandle = metadata.getTableHandle(testSession, tableName).orElseThrow(); + metadata.setColumnType( + testSession, + tableHandle, + metadata.getColumnHandles(testSession, tableHandle).get("test"), + VARCHAR); + + getFutureValue(executeRefreshView(asQualifiedName(qualifiedObjectName("existing_view")))); + assertThat(metadata.getView(testSession, qualifiedObjectName("existing_view")).orElseThrow().getColumns()) + .containsExactly(new ViewColumn("test", TypeId.of("varchar"), Optional.empty())); + } + + @Test + void testTableDroppedWhenRefreshingExistingView() + { + QualifiedObjectName tableName = qualifiedObjectName("existing_table"); + metadata.createTable(testSession, TEST_CATALOG_NAME, someTable(tableName), FAIL); + metadata.createView( + testSession, + qualifiedObjectName("existing_view"), + new ViewDefinition( + "SELECT * FROM test_catalog.schema.existing_table", + Optional.empty(), + Optional.empty(), + ImmutableList.of(new ViewColumn("test", TypeId.of("bigint"), Optional.empty())), + Optional.empty(), + Optional.empty(), + ImmutableList.of()), + ImmutableMap.of(), + false); + + TableHandle tableHandle = metadata.getTableHandle(testSession, tableName).orElseThrow(); + metadata.dropTable(testSession, tableHandle, tableName.asCatalogSchemaTableName()); + + assertThatThrownBy(() -> getFutureValue(executeRefreshView(asQualifiedName(qualifiedObjectName("existing_view"))))) + .isInstanceOf(TrinoException.class) + .hasMessage("line 1:15: Table 'test_catalog.schema.existing_table' does not exist"); + } + + @Test + void testRefreshOnInvokerViewWithRevokedAccessForTable() + { + QualifiedObjectName tableName = qualifiedObjectName("existing_table"); + metadata.createTable(testSession, TEST_CATALOG_NAME, someTable(tableName), FAIL); + metadata.createView( + testSession, + qualifiedObjectName("existing_view"), + new ViewDefinition( + "SELECT * FROM test_catalog.schema.existing_table", + Optional.empty(), + Optional.empty(), + ImmutableList.of(new ViewColumn("test", TypeId.of("bigint"), Optional.empty())), + Optional.empty(), + Optional.empty(), + ImmutableList.of()), + ImmutableMap.of(), + false); + + assertThatThrownBy(() -> getFutureValue( + executeRefreshView( + asQualifiedName(qualifiedObjectName("existing_view")), + new TestingAccessControl(ImmutableSet.of("existing_table"))))) + .isInstanceOf(TrinoException.class) + .hasMessage("Access Denied: Cannot select from columns [test] in table or view test_catalog.schema.existing_table"); + } + + @Test + void testRefreshOnDefinerViewWithRevokedAccessForTable() + { + QualifiedObjectName tableName = qualifiedObjectName("existing_table"); + metadata.createTable(testSession, TEST_CATALOG_NAME, someTable(tableName), FAIL); + metadata.createView( + testSession, + qualifiedObjectName("existing_view"), + new ViewDefinition( + "SELECT * FROM test_catalog.schema.existing_table", + Optional.empty(), + Optional.empty(), + ImmutableList.of(new ViewColumn("test", TypeId.of("bigint"), Optional.empty())), + Optional.empty(), + Optional.of(Identity.ofUser("owner")), + ImmutableList.of()), + ImmutableMap.of(), + false); + + assertThatThrownBy(() -> getFutureValue( + executeRefreshView(asQualifiedName( + qualifiedObjectName("existing_view")), + new TestingAccessControl(ImmutableSet.of("existing_table"))))) + .isInstanceOf(TrinoException.class) + .hasMessage("Access Denied: View owner does not have sufficient privileges: Cannot select from columns [test] in table or view test_catalog.schema.existing_table"); + } + + private ListenableFuture executeRefreshView(QualifiedName viewName) + { + return executeRefreshView(viewName, new AllowAllAccessControl()); + } + + private ListenableFuture executeRefreshView(QualifiedName viewName, AccessControl accessControl) + { + RefreshView statement = new RefreshView(new NodeLocation(1, 1), viewName); + return new RefreshViewTask( + plannerContext, + accessControl, + new TestingGroupProvider(), + parser, + analyzerFactory) + .execute(statement, queryStateMachine, ImmutableList.of(), WarningCollector.NOOP); + } + + private static class TestingAccessControl + extends AllowAllAccessControl + { + private final Set deniedTables; + + public TestingAccessControl(Set deniedTables) + { + this.deniedTables = ImmutableSet.copyOf(requireNonNull(deniedTables, "deniedTables are null")); + } + + @Override + public void checkCanSelectFromColumns(SecurityContext context, QualifiedObjectName tableName, Set columnNames) + { + if (deniedTables.contains(tableName.objectName())) { + denySelectColumns(tableName.toString(), columnNames); + } + } + + @Override + public void checkCanCreateViewWithSelectFromColumns(SecurityContext context, QualifiedObjectName tableName, Set columnNames) + { + if (deniedTables.contains(tableName.objectName())) { + denySelectColumns(tableName.toString(), columnNames); + } + } + } +} diff --git a/core/trino-main/src/test/java/io/trino/metadata/AbstractMockMetadata.java b/core/trino-main/src/test/java/io/trino/metadata/AbstractMockMetadata.java index 9e7b69fefde3..b0477205da1d 100644 --- a/core/trino-main/src/test/java/io/trino/metadata/AbstractMockMetadata.java +++ b/core/trino-main/src/test/java/io/trino/metadata/AbstractMockMetadata.java @@ -620,6 +620,12 @@ public void renameView(Session session, QualifiedObjectName source, QualifiedObj throw new UnsupportedOperationException(); } + @Override + public void refreshView(Session session, QualifiedObjectName viewName, ViewDefinition viewDefinition) + { + throw new UnsupportedOperationException(); + } + @Override public void dropView(Session session, QualifiedObjectName viewName) { diff --git a/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorAccessControl.java b/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorAccessControl.java index c79edce6fd27..59bd0fb8a58c 100644 --- a/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorAccessControl.java +++ b/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorAccessControl.java @@ -58,6 +58,7 @@ import static io.trino.spi.security.AccessDeniedException.denyGrantTablePrivilege; import static io.trino.spi.security.AccessDeniedException.denyInsertTable; import static io.trino.spi.security.AccessDeniedException.denyRefreshMaterializedView; +import static io.trino.spi.security.AccessDeniedException.denyRefreshView; import static io.trino.spi.security.AccessDeniedException.denyRenameColumn; import static io.trino.spi.security.AccessDeniedException.denyRenameMaterializedView; import static io.trino.spi.security.AccessDeniedException.denyRenameSchema; @@ -470,6 +471,16 @@ default void checkCanRefreshMaterializedView(ConnectorSecurityContext context, S denyRefreshMaterializedView(materializedViewName.toString()); } + /** + * Check if identity is allowed to refresh the specified view. + * + * @throws io.trino.spi.security.AccessDeniedException if not allowed + */ + default void checkCanRefreshView(ConnectorSecurityContext context, SchemaTableName viewName) + { + denyRefreshView(viewName.toString()); + } + /** * Check if identity is allowed to set the properties of the specified materialized view. * diff --git a/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorMetadata.java b/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorMetadata.java index fcbd62fc2fd5..9916cf7e73db 100644 --- a/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorMetadata.java +++ b/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorMetadata.java @@ -913,6 +913,14 @@ default void setViewAuthorization(ConnectorSession session, SchemaTableName view throw new TrinoException(NOT_SUPPORTED, "This connector does not support setting an owner on a view"); } + /** + * Refreshes an existing view definition. + */ + default void refreshView(ConnectorSession session, SchemaTableName viewName, ConnectorViewDefinition viewDefinition) + { + throw new TrinoException(NOT_SUPPORTED, "This connector does not support refreshing view definition"); + } + /** * Drop the specified view. */ diff --git a/core/trino-spi/src/main/java/io/trino/spi/security/AccessDeniedException.java b/core/trino-spi/src/main/java/io/trino/spi/security/AccessDeniedException.java index f24e93a30f3d..82880d6dab99 100644 --- a/core/trino-spi/src/main/java/io/trino/spi/security/AccessDeniedException.java +++ b/core/trino-spi/src/main/java/io/trino/spi/security/AccessDeniedException.java @@ -455,6 +455,16 @@ public static void denyRenameView(String viewName, String newViewName, String ex throw new AccessDeniedException(format("Cannot rename view from %s to %s%s", viewName, newViewName, formatExtraInfo(extraInfo))); } + public static void denyRefreshView(String viewName) + { + denyRefreshView(viewName, null); + } + + public static void denyRefreshView(String viewName, String extraInfo) + { + throw new AccessDeniedException(format("Cannot refresh view %s%s", viewName, formatExtraInfo(extraInfo))); + } + /** * @deprecated Use {@link #denySetEntityAuthorization(EntityKindAndName, TrinoPrincipal)} */ diff --git a/core/trino-spi/src/main/java/io/trino/spi/security/SystemAccessControl.java b/core/trino-spi/src/main/java/io/trino/spi/security/SystemAccessControl.java index 343d52eeff7e..944d0721e22a 100644 --- a/core/trino-spi/src/main/java/io/trino/spi/security/SystemAccessControl.java +++ b/core/trino-spi/src/main/java/io/trino/spi/security/SystemAccessControl.java @@ -76,6 +76,7 @@ import static io.trino.spi.security.AccessDeniedException.denyKillQuery; import static io.trino.spi.security.AccessDeniedException.denyReadSystemInformationAccess; import static io.trino.spi.security.AccessDeniedException.denyRefreshMaterializedView; +import static io.trino.spi.security.AccessDeniedException.denyRefreshView; import static io.trino.spi.security.AccessDeniedException.denyRenameColumn; import static io.trino.spi.security.AccessDeniedException.denyRenameMaterializedView; import static io.trino.spi.security.AccessDeniedException.denyRenameSchema; @@ -591,6 +592,16 @@ default void checkCanSetViewAuthorization(SystemSecurityContext context, Catalog denySetEntityAuthorization(new EntityKindAndName("VIEW", List.of(view.getCatalogName(), view.getSchemaTableName().getSchemaName(), view.getSchemaTableName().getTableName())), principal); } + /** + * Check if identity is allowed to refresh the specified view. + * + * @throws io.trino.spi.security.AccessDeniedException if not allowed + */ + default void checkCanRefreshView(SystemSecurityContext context, CatalogSchemaTableName viewName) + { + denyRefreshView(viewName.toString()); + } + /** * Check if identity is allowed to change the specified materialized view's user/role. * diff --git a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/classloader/ClassLoaderSafeConnectorAccessControl.java b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/classloader/ClassLoaderSafeConnectorAccessControl.java index ad44b6a6582c..023fef9dbb85 100644 --- a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/classloader/ClassLoaderSafeConnectorAccessControl.java +++ b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/classloader/ClassLoaderSafeConnectorAccessControl.java @@ -294,6 +294,14 @@ public void checkCanRenameView(ConnectorSecurityContext context, SchemaTableName } } + @Override + public void checkCanRefreshView(ConnectorSecurityContext context, SchemaTableName viewName) + { + try (ThreadContextClassLoader _ = new ThreadContextClassLoader(classLoader)) { + delegate.checkCanRefreshView(context, viewName); + } + } + @Override public void checkCanSetViewAuthorization(ConnectorSecurityContext context, SchemaTableName viewName, TrinoPrincipal principal) { diff --git a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/classloader/ClassLoaderSafeConnectorMetadata.java b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/classloader/ClassLoaderSafeConnectorMetadata.java index d355bcf227c9..a69be62c28c7 100644 --- a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/classloader/ClassLoaderSafeConnectorMetadata.java +++ b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/classloader/ClassLoaderSafeConnectorMetadata.java @@ -675,6 +675,14 @@ public void setViewAuthorization(ConnectorSession session, SchemaTableName viewN } } + @Override + public void refreshView(ConnectorSession session, SchemaTableName viewName, ConnectorViewDefinition viewDefinition) + { + try (ThreadContextClassLoader _ = new ThreadContextClassLoader(classLoader)) { + delegate.refreshView(session, viewName, viewDefinition); + } + } + @Override public void dropView(ConnectorSession session, SchemaTableName viewName) { diff --git a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/AllowAllAccessControl.java b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/AllowAllAccessControl.java index 039785d91c1b..cb67197e2e93 100644 --- a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/AllowAllAccessControl.java +++ b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/AllowAllAccessControl.java @@ -136,6 +136,9 @@ public void checkCanCreateView(ConnectorSecurityContext context, SchemaTableName @Override public void checkCanRenameView(ConnectorSecurityContext context, SchemaTableName viewName, SchemaTableName newViewName) {} + @Override + public void checkCanRefreshView(ConnectorSecurityContext context, SchemaTableName viewName) {} + @Override public void checkCanSetViewAuthorization(ConnectorSecurityContext context, SchemaTableName viewName, TrinoPrincipal principal) {} diff --git a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/AllowAllSystemAccessControl.java b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/AllowAllSystemAccessControl.java index e9a55871e291..5079337b31ab 100644 --- a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/AllowAllSystemAccessControl.java +++ b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/AllowAllSystemAccessControl.java @@ -230,6 +230,9 @@ public void checkCanSetViewAuthorization(SystemSecurityContext context, CatalogS @Override public void checkCanDropView(SystemSecurityContext context, CatalogSchemaTableName view) {} + @Override + public void checkCanRefreshView(SystemSecurityContext context, CatalogSchemaTableName view) {} + @Override public void checkCanCreateViewWithSelectFromColumns(SystemSecurityContext context, CatalogSchemaTableName table, Set columns) {} diff --git a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/FileBasedAccessControl.java b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/FileBasedAccessControl.java index af5ad3ab4345..cb6b7e9aa1b4 100644 --- a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/FileBasedAccessControl.java +++ b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/FileBasedAccessControl.java @@ -81,6 +81,7 @@ import static io.trino.spi.security.AccessDeniedException.denyGrantTablePrivilege; import static io.trino.spi.security.AccessDeniedException.denyInsertTable; import static io.trino.spi.security.AccessDeniedException.denyRefreshMaterializedView; +import static io.trino.spi.security.AccessDeniedException.denyRefreshView; import static io.trino.spi.security.AccessDeniedException.denyRenameColumn; import static io.trino.spi.security.AccessDeniedException.denyRenameMaterializedView; import static io.trino.spi.security.AccessDeniedException.denyRenameSchema; @@ -449,6 +450,15 @@ public void checkCanRenameView(ConnectorSecurityContext context, SchemaTableName } } + @Override + public void checkCanRefreshView(ConnectorSecurityContext context, SchemaTableName viewName) + { + // check if user owns the existing view for refreshing the view + if (!checkTablePermission(context, viewName, OWNERSHIP)) { + denyRefreshView(viewName.toString()); + } + } + @Override public void checkCanSetViewAuthorization(ConnectorSecurityContext context, SchemaTableName viewName, TrinoPrincipal principal) { diff --git a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/FileBasedSystemAccessControl.java b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/FileBasedSystemAccessControl.java index 233cb6c41a33..e87e4c9de3be 100644 --- a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/FileBasedSystemAccessControl.java +++ b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/FileBasedSystemAccessControl.java @@ -97,6 +97,7 @@ import static io.trino.spi.security.AccessDeniedException.denyKillQuery; import static io.trino.spi.security.AccessDeniedException.denyReadSystemInformationAccess; import static io.trino.spi.security.AccessDeniedException.denyRefreshMaterializedView; +import static io.trino.spi.security.AccessDeniedException.denyRefreshView; import static io.trino.spi.security.AccessDeniedException.denyRenameColumn; import static io.trino.spi.security.AccessDeniedException.denyRenameMaterializedView; import static io.trino.spi.security.AccessDeniedException.denyRenameSchema; @@ -726,6 +727,14 @@ public void checkCanSetViewAuthorization(SystemSecurityContext context, CatalogS checkCanSetEntityAuthorization(context, new EntityKindAndName("VIEW", names), principal); } + @Override + public void checkCanRefreshView(SystemSecurityContext context, CatalogSchemaTableName view) + { + if (!checkTablePermission(context, view, OWNERSHIP)) { + denyRefreshView(view.toString()); + } + } + @Override public void checkCanDropView(SystemSecurityContext context, CatalogSchemaTableName view) { diff --git a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/ForwardingConnectorAccessControl.java b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/ForwardingConnectorAccessControl.java index a81b50bcbcdc..c8f6f3ad5fec 100644 --- a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/ForwardingConnectorAccessControl.java +++ b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/ForwardingConnectorAccessControl.java @@ -236,6 +236,12 @@ public void checkCanRenameView(ConnectorSecurityContext context, SchemaTableName delegate().checkCanRenameView(context, viewName, newViewName); } + @Override + public void checkCanRefreshView(ConnectorSecurityContext context, SchemaTableName viewName) + { + delegate().checkCanRefreshView(context, viewName); + } + @Override public void checkCanSetViewAuthorization(ConnectorSecurityContext context, SchemaTableName viewName, TrinoPrincipal principal) { diff --git a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/ForwardingSystemAccessControl.java b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/ForwardingSystemAccessControl.java index aa7ea67bc9e9..1ed4f1315c6e 100644 --- a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/ForwardingSystemAccessControl.java +++ b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/security/ForwardingSystemAccessControl.java @@ -335,6 +335,12 @@ public void checkCanSetViewAuthorization(SystemSecurityContext context, CatalogS delegate().checkCanSetViewAuthorization(context, view, principal); } + @Override + public void checkCanRefreshView(SystemSecurityContext context, CatalogSchemaTableName viewName) + { + delegate().checkCanRefreshView(context, viewName); + } + @Override public void checkCanDropView(SystemSecurityContext context, CatalogSchemaTableName view) { diff --git a/plugin/trino-hive/src/main/java/io/trino/plugin/hive/security/SqlStandardAccessControl.java b/plugin/trino-hive/src/main/java/io/trino/plugin/hive/security/SqlStandardAccessControl.java index 061cfb818893..000091afab4b 100644 --- a/plugin/trino-hive/src/main/java/io/trino/plugin/hive/security/SqlStandardAccessControl.java +++ b/plugin/trino-hive/src/main/java/io/trino/plugin/hive/security/SqlStandardAccessControl.java @@ -85,6 +85,7 @@ import static io.trino.spi.security.AccessDeniedException.denyGrantTablePrivilege; import static io.trino.spi.security.AccessDeniedException.denyInsertTable; import static io.trino.spi.security.AccessDeniedException.denyRefreshMaterializedView; +import static io.trino.spi.security.AccessDeniedException.denyRefreshView; import static io.trino.spi.security.AccessDeniedException.denyRenameColumn; import static io.trino.spi.security.AccessDeniedException.denyRenameMaterializedView; import static io.trino.spi.security.AccessDeniedException.denyRenameSchema; @@ -238,6 +239,14 @@ public void checkCanSetViewComment(ConnectorSecurityContext context, SchemaTable } } + @Override + public void checkCanRefreshView(ConnectorSecurityContext context, SchemaTableName viewName) + { + if (!checkTablePermission(context, viewName, UPDATE, false)) { + denyRefreshView(viewName.toString()); + } + } + @Override public void checkCanSetColumnComment(ConnectorSecurityContext context, SchemaTableName tableName) { diff --git a/plugin/trino-lakehouse/src/main/java/io/trino/plugin/lakehouse/LakehouseMetadata.java b/plugin/trino-lakehouse/src/main/java/io/trino/plugin/lakehouse/LakehouseMetadata.java index 99cb65cff0e3..bbc14a6bae8b 100644 --- a/plugin/trino-lakehouse/src/main/java/io/trino/plugin/lakehouse/LakehouseMetadata.java +++ b/plugin/trino-lakehouse/src/main/java/io/trino/plugin/lakehouse/LakehouseMetadata.java @@ -39,6 +39,7 @@ import io.trino.plugin.iceberg.IcebergWritableTableHandle; import io.trino.plugin.iceberg.procedure.IcebergTableExecuteHandle; import io.trino.spi.RefreshType; +import io.trino.spi.TrinoException; import io.trino.spi.connector.AggregateFunction; import io.trino.spi.connector.AggregationApplicationResult; import io.trino.spi.connector.BeginTableExecuteResult; @@ -111,6 +112,7 @@ import static io.trino.plugin.iceberg.IcebergTableName.isIcebergTableName; import static io.trino.plugin.iceberg.IcebergTableName.isMaterializedViewStorage; import static io.trino.plugin.lakehouse.LakehouseTableProperties.getTableType; +import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED; import static java.util.Objects.requireNonNull; public class LakehouseMetadata @@ -602,6 +604,12 @@ public void setViewAuthorization(ConnectorSession session, SchemaTableName viewN hiveMetadata.setViewAuthorization(session, viewName, principal); } + @Override + public void refreshView(ConnectorSession session, SchemaTableName viewName, ConnectorViewDefinition viewDefinition) + { + throw new TrinoException(NOT_SUPPORTED, "This connector does not support refreshing view definition"); + } + @Override public void dropView(ConnectorSession session, SchemaTableName viewName) { diff --git a/service/trino-verifier/src/main/java/io/trino/verifier/VerifyCommand.java b/service/trino-verifier/src/main/java/io/trino/verifier/VerifyCommand.java index 889c93e4b832..e82424264ca0 100644 --- a/service/trino-verifier/src/main/java/io/trino/verifier/VerifyCommand.java +++ b/service/trino-verifier/src/main/java/io/trino/verifier/VerifyCommand.java @@ -48,6 +48,7 @@ import io.trino.sql.tree.ExplainAnalyze; import io.trino.sql.tree.Insert; import io.trino.sql.tree.RefreshMaterializedView; +import io.trino.sql.tree.RefreshView; import io.trino.sql.tree.RenameColumn; import io.trino.sql.tree.RenameMaterializedView; import io.trino.sql.tree.RenameTable; @@ -390,6 +391,9 @@ private static QueryType statementToQueryType(Statement statement) if (statement instanceof RefreshMaterializedView) { return MODIFY; } + if (statement instanceof RefreshView) { + return MODIFY; + } if (statement instanceof DropMaterializedView) { return MODIFY; } From 7ad3d422fbc1c2dd5caf30b050fb1e915c334c06 Mon Sep 17 00:00:00 2001 From: "praveenkrishna.d" Date: Mon, 2 Jun 2025 23:25:23 +0530 Subject: [PATCH 3/3] Add support for refreshing views in memory connector --- .../deltalake/TestDeltaLakeConnectorTest.java | 1 + .../plugin/hive/BaseHiveConnectorTest.java | 1 + .../iceberg/BaseIcebergConnectorTest.java | 1 + .../lakehouse/TestLakehouseConnectorTest.java | 1 + .../trino/plugin/memory/MemoryMetadata.java | 12 ++++ .../io/trino/testing/BaseConnectorTest.java | 62 +++++++++++++++++++ .../testing/TestingConnectorBehavior.java | 1 + 7 files changed, 79 insertions(+) diff --git a/plugin/trino-delta-lake/src/test/java/io/trino/plugin/deltalake/TestDeltaLakeConnectorTest.java b/plugin/trino-delta-lake/src/test/java/io/trino/plugin/deltalake/TestDeltaLakeConnectorTest.java index 44e992e54761..5b8a21f33a8b 100644 --- a/plugin/trino-delta-lake/src/test/java/io/trino/plugin/deltalake/TestDeltaLakeConnectorTest.java +++ b/plugin/trino-delta-lake/src/test/java/io/trino/plugin/deltalake/TestDeltaLakeConnectorTest.java @@ -182,6 +182,7 @@ protected boolean hasBehavior(TestingConnectorBehavior connectorBehavior) SUPPORTS_DROP_FIELD, SUPPORTS_LIMIT_PUSHDOWN, SUPPORTS_PREDICATE_PUSHDOWN, + SUPPORTS_REFRESH_VIEW, SUPPORTS_RENAME_FIELD, SUPPORTS_RENAME_SCHEMA, SUPPORTS_SET_COLUMN_TYPE, diff --git a/plugin/trino-hive/src/test/java/io/trino/plugin/hive/BaseHiveConnectorTest.java b/plugin/trino-hive/src/test/java/io/trino/plugin/hive/BaseHiveConnectorTest.java index a38e67454967..3e058864ca9e 100644 --- a/plugin/trino-hive/src/test/java/io/trino/plugin/hive/BaseHiveConnectorTest.java +++ b/plugin/trino-hive/src/test/java/io/trino/plugin/hive/BaseHiveConnectorTest.java @@ -257,6 +257,7 @@ protected boolean hasBehavior(TestingConnectorBehavior connectorBehavior) SUPPORTS_DROP_FIELD, SUPPORTS_MERGE, SUPPORTS_NOT_NULL_CONSTRAINT, + SUPPORTS_REFRESH_VIEW, SUPPORTS_RENAME_FIELD, SUPPORTS_SET_COLUMN_TYPE, SUPPORTS_TOPN_PUSHDOWN, diff --git a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/BaseIcebergConnectorTest.java b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/BaseIcebergConnectorTest.java index ef53d19d4330..e1792553f336 100644 --- a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/BaseIcebergConnectorTest.java +++ b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/BaseIcebergConnectorTest.java @@ -241,6 +241,7 @@ protected boolean hasBehavior(TestingConnectorBehavior connectorBehavior) SUPPORTS_REPORTING_WRITTEN_BYTES -> true; case SUPPORTS_ADD_COLUMN_NOT_NULL_CONSTRAINT, SUPPORTS_DEFAULT_COLUMN_VALUE, + SUPPORTS_REFRESH_VIEW, SUPPORTS_RENAME_MATERIALIZED_VIEW_ACROSS_SCHEMAS, SUPPORTS_TOPN_PUSHDOWN -> false; default -> super.hasBehavior(connectorBehavior); diff --git a/plugin/trino-lakehouse/src/test/java/io/trino/plugin/lakehouse/TestLakehouseConnectorTest.java b/plugin/trino-lakehouse/src/test/java/io/trino/plugin/lakehouse/TestLakehouseConnectorTest.java index 1d56af7ead60..c99982150eef 100644 --- a/plugin/trino-lakehouse/src/test/java/io/trino/plugin/lakehouse/TestLakehouseConnectorTest.java +++ b/plugin/trino-lakehouse/src/test/java/io/trino/plugin/lakehouse/TestLakehouseConnectorTest.java @@ -90,6 +90,7 @@ protected boolean hasBehavior(TestingConnectorBehavior connectorBehavior) SUPPORTS_REPORTING_WRITTEN_BYTES -> true; case SUPPORTS_ADD_COLUMN_NOT_NULL_CONSTRAINT, SUPPORTS_DEFAULT_COLUMN_VALUE, + SUPPORTS_REFRESH_VIEW, SUPPORTS_RENAME_MATERIALIZED_VIEW_ACROSS_SCHEMAS, SUPPORTS_TOPN_PUSHDOWN -> false; default -> super.hasBehavior(connectorBehavior); diff --git a/plugin/trino-memory/src/main/java/io/trino/plugin/memory/MemoryMetadata.java b/plugin/trino-memory/src/main/java/io/trino/plugin/memory/MemoryMetadata.java index 4b8765a09fa6..70e6bb8c652f 100644 --- a/plugin/trino-memory/src/main/java/io/trino/plugin/memory/MemoryMetadata.java +++ b/plugin/trino-memory/src/main/java/io/trino/plugin/memory/MemoryMetadata.java @@ -559,6 +559,18 @@ public synchronized void renameView(ConnectorSession session, SchemaTableName vi views.put(newViewName, views.remove(viewName)); } + @Override + public synchronized void refreshView(ConnectorSession session, SchemaTableName viewName, ConnectorViewDefinition viewDefinition) + { + checkSchemaExists(viewName.getSchemaName()); + + if (!tableIds.containsKey(viewName)) { + throw new TrinoException(NOT_FOUND, "View not found: " + viewName); + } + + views.replace(viewName, viewDefinition); + } + @Override public synchronized void dropView(ConnectorSession session, SchemaTableName viewName) { diff --git a/testing/trino-testing/src/main/java/io/trino/testing/BaseConnectorTest.java b/testing/trino-testing/src/main/java/io/trino/testing/BaseConnectorTest.java index c7176f613e8a..45f9d070c080 100644 --- a/testing/trino-testing/src/main/java/io/trino/testing/BaseConnectorTest.java +++ b/testing/trino-testing/src/main/java/io/trino/testing/BaseConnectorTest.java @@ -140,6 +140,7 @@ import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_MULTI_STATEMENT_WRITES; import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_NEGATIVE_DATE; import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_NOT_NULL_CONSTRAINT; +import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_REFRESH_VIEW; import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_RENAME_COLUMN; import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_RENAME_FIELD; import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_RENAME_MATERIALIZED_VIEW; @@ -989,6 +990,67 @@ public void testView() .doesNotContain(testView); } + @Test + public void testRefreshView() + { + if (!hasBehavior(SUPPORTS_REFRESH_VIEW)) { + if (hasBehavior(SUPPORTS_CREATE_VIEW)) { + try (TestView testView = new TestView(getQueryRunner()::execute, "test_view", " SELECT * FROM nation")) { + assertQueryFails("ALTER VIEW %s REFRESH".formatted(testView.getName()), "This connector does not support refreshing view definition"); + } + } + return; + } + + if (!hasBehavior(SUPPORTS_CREATE_TABLE) && !hasBehavior(SUPPORTS_ADD_COLUMN)) { + throw new AssertionError("Cannot test ALTER VIEW REFRESH without CREATE TABLE, the test needs to be implemented in a connector-specific way"); + } + + try (TestTable table = newTrinoTable("test_table", "(id BIGINT, column_to_dropped BIGINT, column_to_be_renamed BIGINT, column_with_comment BIGINT)", ImmutableList.of("1, 2, 3, 4")); + TestView view = new TestView(getQueryRunner()::execute, "test_view", " SELECT * FROM %s".formatted(table.getName()))) { + assertQuery("SELECT * FROM " + view.getName(), "VALUES (1, 2, 3, 4)"); + + assertUpdate("ALTER TABLE %s ADD COLUMN new_column BIGINT".formatted(table.getName())); + assertQueryFails( + "SELECT * FROM " + view.getName(), + ".*is stale or in invalid state: stored view column count \\(4\\) does not match column count derived from the view query analysis \\(5\\)"); + + assertUpdate("ALTER VIEW %s REFRESH".formatted(view.getName())); + assertQuery("SELECT * FROM " + view.getName(), "VALUES (1, 2, 3, 4, null)"); + + if (hasBehavior(SUPPORTS_RENAME_COLUMN)) { + assertUpdate("ALTER TABLE %s RENAME COLUMN column_to_be_renamed TO renamed_column".formatted(table.getName())); + assertQueryFails( + "SELECT * FROM %s".formatted(view.getName()), + ".*is stale or in invalid state: column \\[renamed_column] of type bigint projected from query view at position 2 has a different name from column \\[column_to_be_renamed] of type bigint stored in view definition"); + assertUpdate("ALTER VIEW %s REFRESH".formatted(view.getName())); + assertQuery("SELECT * FROM " + view.getName(), "VALUES (1, 2, 3, 4, null)"); + } + + if (hasBehavior(SUPPORTS_COMMENT_ON_COLUMN)) { + assertUpdate("COMMENT ON COLUMN %s.column_with_comment IS 'test comment'".formatted(view.getName())); + assertThat(getColumnComment(view.getName(), "column_with_comment")).isEqualTo("test comment"); + + // Add another column + assertUpdate("ALTER TABLE %s ADD COLUMN new_column_2 BIGINT".formatted(table.getName())); + assertUpdate("ALTER VIEW %s REFRESH".formatted(view.getName())); + assertThat(getColumnComment(view.getName(), "column_with_comment")).isEqualTo("test comment"); + + assertUpdate("ALTER TABLE %s RENAME COLUMN column_with_comment TO renamed_new_column".formatted(table.getName())); + } + + if (hasBehavior(SUPPORTS_DROP_COLUMN)) { + assertUpdate("ALTER TABLE %s DROP COLUMN column_to_dropped".formatted(table.getName())); + assertQueryFails( + "SELECT * FROM " + view.getName(), + ".*is stale or in invalid state: stored view column count \\(5\\) does not match column count derived from the view query analysis \\(4\\)"); + + assertUpdate("ALTER VIEW %s REFRESH".formatted(view.getName())); + assertQuery("SELECT * FROM " + view.getName(), "VALUES (1, 3, 4, null)"); + } + } + } + @Test public void testCreateViewSchemaNotFound() { diff --git a/testing/trino-testing/src/main/java/io/trino/testing/TestingConnectorBehavior.java b/testing/trino-testing/src/main/java/io/trino/testing/TestingConnectorBehavior.java index 740cc1286e0c..a0e8b3ac8f6b 100644 --- a/testing/trino-testing/src/main/java/io/trino/testing/TestingConnectorBehavior.java +++ b/testing/trino-testing/src/main/java/io/trino/testing/TestingConnectorBehavior.java @@ -104,6 +104,7 @@ public enum TestingConnectorBehavior SUPPORTS_CREATE_VIEW, SUPPORTS_COMMENT_ON_VIEW(and(SUPPORTS_CREATE_VIEW, SUPPORTS_COMMENT_ON_TABLE)), SUPPORTS_COMMENT_ON_VIEW_COLUMN(SUPPORTS_COMMENT_ON_VIEW), + SUPPORTS_REFRESH_VIEW(SUPPORTS_CREATE_VIEW), SUPPORTS_CREATE_MATERIALIZED_VIEW, SUPPORTS_CREATE_MATERIALIZED_VIEW_GRACE_PERIOD(SUPPORTS_CREATE_MATERIALIZED_VIEW),