From df2503a5b92917ce91def7cf2dbffcbab2624479 Mon Sep 17 00:00:00 2001 From: Piotr Rzysko Date: Tue, 18 Nov 2025 07:49:21 +0100 Subject: [PATCH 1/3] Add WHEN STALE FAIL/INLINE syntax --- .../antlr4/io/trino/grammar/sql/SqlBase.g4 | 10 ++- .../io/trino/grammar/sql/TestSqlKeywords.java | 3 + .../trino/sql/analyzer/StatementAnalyzer.java | 3 + .../trino/sql/rewrite/ShowQueriesRewrite.java | 1 + .../TestCreateMaterializedViewTask.java | 5 ++ .../main/java/io/trino/sql/SqlFormatter.java | 2 + .../java/io/trino/sql/parser/AstBuilder.java | 10 +++ .../sql/tree/CreateMaterializedView.java | 18 ++++- .../java/io/trino/sql/TestSqlFormatter.java | 22 ++++++ .../io/trino/sql/parser/TestSqlParser.java | 78 +++++++++++++++++++ 10 files changed, 148 insertions(+), 4 deletions(-) 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 a6f8fd9951b..02b83dbdb7f 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 @@ -106,6 +106,7 @@ statement | CREATE (OR REPLACE)? MATERIALIZED VIEW (IF NOT EXISTS)? qualifiedName (GRACE PERIOD interval)? + (WHEN STALE (INLINE | FAIL))? (COMMENT string)? (WITH properties)? AS rootQuery #createMaterializedView | CREATE (OR REPLACE)? VIEW qualifiedName @@ -1035,10 +1036,10 @@ nonReserved | CALL | CALLED | CASCADE | CATALOG | CATALOGS | COLUMN | COLUMNS | COMMENT | COMMIT | COMMITTED | CONDITIONAL | COPARTITION | CORRESPONDING | COUNT | CURRENT | DATA | DATE | DAY | DECLARE | DEFAULT | DEFINE | DEFINER | DENY | DESC | DESCRIPTOR | DETERMINISTIC | DISTRIBUTED | DO | DOUBLE | ELSEIF | EMPTY | ENCODING | ERROR | EXCLUDING | EXECUTE | EXPLAIN - | FAST | FETCH | FILTER | FINAL | FIRST | FOLLOWING | FORMAT | FORWARD | FUNCTION | FUNCTIONS + | FAIL | FAST | FETCH | FILTER | FINAL | FIRST | FOLLOWING | FORMAT | FORWARD | FUNCTION | FUNCTIONS | GRACE | GRANT | GRANTED | GRANTS | GRAPHVIZ | GROUPS | HOUR - | IF | IGNORE | IMMEDIATE | INCLUDING | INITIAL | INPUT | INTERVAL | INVOKER | IO | ITERATE | ISOLATION + | IF | IGNORE | IMMEDIATE | INCLUDING | INITIAL | INLINE | INPUT | INTERVAL | INVOKER | IO | ITERATE | ISOLATION | JSON | KEEP | KEY | KEYS | LANGUAGE | LAST | LATERAL | LEADING | LEAVE | LEVEL | LIMIT | LOCAL | LOGICAL | LOOP @@ -1049,7 +1050,7 @@ nonReserved | QUOTES | RANGE | READ | REFRESH | RENAME | REPEAT | REPEATABLE | REPLACE | RESET | RESPECT | RESTRICT | RETURN | RETURNING | RETURNS | REVOKE | ROLE | ROLES | ROLLBACK | ROW | ROWS | RUNNING | SCALAR | SCHEMA | SCHEMAS | SECOND | SECURITY | SEEK | SERIALIZABLE | SESSION | SET | SETS - | SHOW | SOME | START | STATS | SUBSET | SUBSTRING | SYSTEM + | SHOW | SOME | STALE | START | STATS | SUBSET | SUBSTRING | SYSTEM | TABLES | TABLESAMPLE | TEXT | TEXT_STRING | TIES | TIME | TIMESTAMP | TO | TRAILING | TRANSACTION | TRUNCATE | TRY_CAST | TYPE | UNBOUNDED | UNCOMMITTED | UNCONDITIONAL | UNIQUE | UNKNOWN | UNMATCHED | UNTIL | UPDATE | USE | USER | UTF16 | UTF32 | UTF8 | VALIDATE | VALUE | VERBOSE | VERSION | VIEW @@ -1141,6 +1142,7 @@ EXECUTE: 'EXECUTE'; EXISTS: 'EXISTS'; EXPLAIN: 'EXPLAIN'; EXTRACT: 'EXTRACT'; +FAIL: 'FAIL'; FALSE: 'FALSE'; FAST: 'FAST'; FETCH: 'FETCH'; @@ -1171,6 +1173,7 @@ IMMEDIATE: 'IMMEDIATE'; IN: 'IN'; INCLUDING: 'INCLUDING'; INITIAL: 'INITIAL'; +INLINE: 'INLINE'; INNER: 'INNER'; INPUT: 'INPUT'; INSERT: 'INSERT'; @@ -1301,6 +1304,7 @@ SET: 'SET'; SETS: 'SETS'; SHOW: 'SHOW'; SOME: 'SOME'; +STALE: 'STALE'; START: 'START'; STATS: 'STATS'; SUBSET: 'SUBSET'; diff --git a/core/trino-grammar/src/test/java/io/trino/grammar/sql/TestSqlKeywords.java b/core/trino-grammar/src/test/java/io/trino/grammar/sql/TestSqlKeywords.java index 0431ff9b28b..cb44176dc6e 100644 --- a/core/trino-grammar/src/test/java/io/trino/grammar/sql/TestSqlKeywords.java +++ b/core/trino-grammar/src/test/java/io/trino/grammar/sql/TestSqlKeywords.java @@ -109,6 +109,7 @@ public void test() "EXISTS", "EXPLAIN", "EXTRACT", + "FAIL", "FALSE", "FAST", "FETCH", @@ -139,6 +140,7 @@ public void test() "IN", "INCLUDING", "INITIAL", + "INLINE", "INNER", "INPUT", "INSERT", @@ -270,6 +272,7 @@ public void test() "SHOW", "SKIP", "SOME", + "STALE", "START", "STATS", "STRING", 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 858a828bb12..9906babc4da 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 @@ -1479,6 +1479,9 @@ protected Scope visitCreateMaterializedView(CreateMaterializedView node, Optiona throw semanticException(NOT_SUPPORTED, node, "'CREATE OR REPLACE' and 'IF NOT EXISTS' clauses can not be used together"); } node.getGracePeriod().ifPresent(gracePeriod -> analyzeExpression(gracePeriod, Scope.create())); + if (node.getWhenStaleBehavior().isPresent()) { + throw new TrinoException(NOT_SUPPORTED, "WHEN STALE is not supported yet"); + } // analyze the query that creates the view StatementAnalyzer analyzer = statementAnalyzerFactory.createStatementAnalyzer(analysis, session, warningCollector, CorrelationSupport.ALLOWED); diff --git a/core/trino-main/src/main/java/io/trino/sql/rewrite/ShowQueriesRewrite.java b/core/trino-main/src/main/java/io/trino/sql/rewrite/ShowQueriesRewrite.java index 51d0cb1eb10..fba03ce4215 100644 --- a/core/trino-main/src/main/java/io/trino/sql/rewrite/ShowQueriesRewrite.java +++ b/core/trino-main/src/main/java/io/trino/sql/rewrite/ShowQueriesRewrite.java @@ -562,6 +562,7 @@ private Query showCreateMaterializedView(ShowCreate node) false, false, Optional.empty(), // TODO support GRACE PERIOD + Optional.empty(), // TODO support WHEN STALE propertyNodes, viewDefinition.get().getComment())).trim(); return singleValueQuery("Create Materialized View", sql); diff --git a/core/trino-main/src/test/java/io/trino/execution/TestCreateMaterializedViewTask.java b/core/trino-main/src/test/java/io/trino/execution/TestCreateMaterializedViewTask.java index 8168b2e34b2..0074a841d3a 100644 --- a/core/trino-main/src/test/java/io/trino/execution/TestCreateMaterializedViewTask.java +++ b/core/trino-main/src/test/java/io/trino/execution/TestCreateMaterializedViewTask.java @@ -127,6 +127,7 @@ void testCreateMaterializedViewIfNotExists() false, true, Optional.empty(), + Optional.empty(), ImmutableList.of(), Optional.empty()); @@ -147,6 +148,7 @@ void testCreateMaterializedViewWithExistingView() false, false, Optional.empty(), + Optional.empty(), ImmutableList.of(), Optional.empty()); @@ -169,6 +171,7 @@ void testCreateMaterializedViewWithInvalidProperty() false, true, Optional.empty(), + Optional.empty(), ImmutableList.of(new Property(new NodeLocation(1, 88), new Identifier("baz"), new StringLiteral("abc"))), Optional.empty()); @@ -191,6 +194,7 @@ void testCreateMaterializedViewWithDefaultProperties() false, true, Optional.empty(), + Optional.empty(), ImmutableList.of( new Property(new Identifier("foo")), // set foo to DEFAULT new Property(new Identifier("bar"))), // set bar to DEFAULT @@ -217,6 +221,7 @@ public void testCreateDenyPermission() false, true, Optional.empty(), + Optional.empty(), ImmutableList.of(), Optional.empty()); queryRunner.getAccessControl().deny(privilege("test_mv_deny", CREATE_MATERIALIZED_VIEW)); 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 4185d540f76..f65fb618c25 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 @@ -1262,6 +1262,8 @@ protected Void visitCreateMaterializedView(CreateMaterializedView node, Integer builder.append(formatName(node.getName())); node.getGracePeriod().ifPresent(interval -> builder.append("\nGRACE PERIOD ").append(formatExpression(interval))); + node.getWhenStaleBehavior().ifPresent(whenStale -> + builder.append("\nWHEN STALE ").append(whenStale.name())); node.getComment().ifPresent(comment -> builder .append("\nCOMMENT ") .append(formatStringLiteral(comment))); 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 b8b2a76198a..8a33fe5e491 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 @@ -57,6 +57,7 @@ import io.trino.sql.tree.CreateCatalog; import io.trino.sql.tree.CreateFunction; import io.trino.sql.tree.CreateMaterializedView; +import io.trino.sql.tree.CreateMaterializedView.WhenStaleBehavior; import io.trino.sql.tree.CreateRole; import io.trino.sql.tree.CreateSchema; import io.trino.sql.tree.CreateTable; @@ -613,6 +614,14 @@ public Node visitCreateMaterializedView(SqlBaseParser.CreateMaterializedViewCont gracePeriod = Optional.of((IntervalLiteral) visit(context.interval())); } + Optional whenStale = Optional.empty(); + if (context.INLINE() != null) { + whenStale = Optional.of(WhenStaleBehavior.INLINE); + } + else if (context.FAIL() != null) { + whenStale = Optional.of(WhenStaleBehavior.FAIL); + } + Optional comment = Optional.empty(); if (context.COMMENT() != null) { comment = Optional.of(visitString(context.string()).getValue()); @@ -630,6 +639,7 @@ public Node visitCreateMaterializedView(SqlBaseParser.CreateMaterializedViewCont context.REPLACE() != null, context.EXISTS() != null, gracePeriod, + whenStale, properties, comment); } diff --git a/core/trino-parser/src/main/java/io/trino/sql/tree/CreateMaterializedView.java b/core/trino-parser/src/main/java/io/trino/sql/tree/CreateMaterializedView.java index 2c131614d8f..023670abd43 100644 --- a/core/trino-parser/src/main/java/io/trino/sql/tree/CreateMaterializedView.java +++ b/core/trino-parser/src/main/java/io/trino/sql/tree/CreateMaterializedView.java @@ -25,11 +25,18 @@ public class CreateMaterializedView extends Statement { + public enum WhenStaleBehavior + { + INLINE, + FAIL, + } + private final QualifiedName name; private final Query query; private final boolean replace; private final boolean notExists; private final Optional gracePeriod; + private final Optional whenStaleBehavior; private final List properties; private final Optional comment; @@ -40,6 +47,7 @@ public CreateMaterializedView( boolean replace, boolean notExists, Optional gracePeriod, + Optional whenStaleBehavior, List properties, Optional comment) { @@ -49,6 +57,7 @@ public CreateMaterializedView( this.replace = replace; this.notExists = notExists; this.gracePeriod = requireNonNull(gracePeriod, "gracePeriod is null"); + this.whenStaleBehavior = requireNonNull(whenStaleBehavior, "whenStaleBehavior is null"); this.properties = ImmutableList.copyOf(requireNonNull(properties, "properties is null")); this.comment = requireNonNull(comment, "comment is null"); } @@ -78,6 +87,11 @@ public Optional getGracePeriod() return gracePeriod; } + public Optional getWhenStaleBehavior() + { + return whenStaleBehavior; + } + public List getProperties() { return properties; @@ -106,7 +120,7 @@ public List getChildren() @Override public int hashCode() { - return Objects.hash(name, query, replace, notExists, gracePeriod, properties, comment); + return Objects.hash(name, query, replace, notExists, gracePeriod, whenStaleBehavior, properties, comment); } @Override @@ -124,6 +138,7 @@ public boolean equals(Object obj) && replace == o.replace && notExists == o.notExists && Objects.equals(gracePeriod, o.gracePeriod) + && Objects.equals(whenStaleBehavior, o.whenStaleBehavior) && Objects.equals(properties, o.properties) && Objects.equals(comment, o.comment); } @@ -137,6 +152,7 @@ public String toString() .add("replace", replace) .add("notExists", notExists) .add("gracePeriod", gracePeriod) + .add("whenStaleBehavior", whenStaleBehavior) .add("properties", properties) .add("comment", comment) .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 1b21adfb4a4..4a317e6d597 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 @@ -23,6 +23,7 @@ import io.trino.sql.tree.CreateBranch; import io.trino.sql.tree.CreateCatalog; import io.trino.sql.tree.CreateMaterializedView; +import io.trino.sql.tree.CreateMaterializedView.WhenStaleBehavior; import io.trino.sql.tree.CreateTable; import io.trino.sql.tree.CreateTableAsSelect; import io.trino.sql.tree.CreateView; @@ -448,6 +449,7 @@ public void testCreateMaterializedView() false, false, Optional.empty(), + Optional.empty(), ImmutableList.of(), Optional.empty()))) .isEqualTo("CREATE MATERIALIZED VIEW test_mv AS\n" + @@ -462,6 +464,7 @@ public void testCreateMaterializedView() false, false, Optional.empty(), + Optional.empty(), ImmutableList.of(), Optional.of("攻殻機動隊")))) .isEqualTo("CREATE MATERIALIZED VIEW test_mv\n" + @@ -469,6 +472,25 @@ public void testCreateMaterializedView() "SELECT *\n" + "FROM\n" + " test_base\n"); + assertThat(formatSql( + new CreateMaterializedView( + new NodeLocation(1, 1), + QualifiedName.of("test_mv"), + simpleQuery(selectList(new AllColumns()), table(QualifiedName.of("test_base"))), + false, + false, + Optional.empty(), + Optional.of(WhenStaleBehavior.FAIL), + ImmutableList.of(), + Optional.empty()))) + .isEqualTo( + """ + CREATE MATERIALIZED VIEW test_mv + WHEN STALE FAIL AS + SELECT * + FROM + test_base + """); } @Test 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 a1ab2a78db8..8f9f0711e72 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 @@ -43,6 +43,7 @@ import io.trino.sql.tree.CreateBranch; import io.trino.sql.tree.CreateCatalog; import io.trino.sql.tree.CreateMaterializedView; +import io.trino.sql.tree.CreateMaterializedView.WhenStaleBehavior; import io.trino.sql.tree.CreateRole; import io.trino.sql.tree.CreateSchema; import io.trino.sql.tree.CreateTable; @@ -5893,6 +5894,7 @@ public void testCreateMaterializedView() false, false, Optional.empty(), + Optional.empty(), ImmutableList.of(), Optional.empty())); @@ -5935,6 +5937,7 @@ public void testCreateMaterializedView() true, false, Optional.empty(), + Optional.empty(), ImmutableList.of(), Optional.of("A simple materialized view"))); @@ -5970,6 +5973,79 @@ public void testCreateMaterializedView() false, false, Optional.of(new IntervalLiteral(new NodeLocation(1, 41), "2", Sign.POSITIVE, IntervalField.DAY, Optional.empty())), + Optional.empty(), + ImmutableList.of(), + Optional.empty())); + + // WHEN STALE FAIL + assertThat(statement("CREATE MATERIALIZED VIEW a WHEN STALE FAIL AS SELECT * FROM t")) + .isEqualTo(new CreateMaterializedView( + new NodeLocation(1, 1), + QualifiedName.of(ImmutableList.of(new Identifier(new NodeLocation(1, 26), "a", false))), + new Query( + new NodeLocation(1, 47), + ImmutableList.of(), + ImmutableList.of(), + Optional.empty(), + new QuerySpecification( + new NodeLocation(1, 47), + new Select( + new NodeLocation(1, 47), + false, + ImmutableList.of(new AllColumns(new NodeLocation(1, 54), Optional.empty(), ImmutableList.of()))), + Optional.of(new Table( + new NodeLocation(1, 61), + QualifiedName.of(ImmutableList.of(new Identifier(new NodeLocation(1, 61), "t", false))))), + Optional.empty(), + Optional.empty(), + Optional.empty(), + ImmutableList.of(), + Optional.empty(), + Optional.empty(), + Optional.empty()), + Optional.empty(), + Optional.empty(), + Optional.empty()), + false, + false, + Optional.empty(), + Optional.of(WhenStaleBehavior.FAIL), + ImmutableList.of(), + Optional.empty())); + + // WHEN STALE INLINE + assertThat(statement("CREATE MATERIALIZED VIEW a WHEN STALE INLINE AS SELECT * FROM t")) + .isEqualTo(new CreateMaterializedView( + new NodeLocation(1, 1), + QualifiedName.of(ImmutableList.of(new Identifier(new NodeLocation(1, 26), "a", false))), + new Query( + new NodeLocation(1, 49), + ImmutableList.of(), + ImmutableList.of(), + Optional.empty(), + new QuerySpecification( + new NodeLocation(1, 49), + new Select( + new NodeLocation(1, 49), + false, + ImmutableList.of(new AllColumns(new NodeLocation(1, 56), Optional.empty(), ImmutableList.of()))), + Optional.of(new Table( + new NodeLocation(1, 63), + QualifiedName.of(ImmutableList.of(new Identifier(new NodeLocation(1, 63), "t", false))))), + Optional.empty(), + Optional.empty(), + Optional.empty(), + ImmutableList.of(), + Optional.empty(), + Optional.empty(), + Optional.empty()), + Optional.empty(), + Optional.empty(), + Optional.empty()), + false, + false, + Optional.empty(), + Optional.of(WhenStaleBehavior.INLINE), ImmutableList.of(), Optional.empty())); @@ -6016,6 +6092,7 @@ public void testCreateMaterializedView() true, false, Optional.empty(), + Optional.empty(), ImmutableList.of(new Property( new NodeLocation(2, 7), new Identifier(new NodeLocation(2, 7), "partitioned_by", false), @@ -6112,6 +6189,7 @@ AS WITH a (t, u) AS (SELECT * FROM x), b AS (SELECT * FROM a) TABLE b true, false, Optional.empty(), + Optional.empty(), ImmutableList.of(new Property( new NodeLocation(2, 7), new Identifier(new NodeLocation(2, 7), "partitioned_by", false), From 976cbc3008a5096e145d412a55583d078a52fa7c Mon Sep 17 00:00:00 2001 From: Piotr Rzysko Date: Tue, 18 Nov 2025 07:49:22 +0100 Subject: [PATCH 2/3] Add when stale behavior in ConnectorMaterializedViewDefinition This is a preparatory change that allows connectors to store information about the WHEN STALE behavior. The option cannot yet be used in practice, as execution will still fail with a NOT_SUPPORTED error. --- .../execution/CreateMaterializedViewTask.java | 11 +++++++++++ .../metadata/MaterializedViewDefinition.java | 11 +++++++++++ .../io/trino/metadata/MetadataManager.java | 1 + .../trino/sql/analyzer/StatementAnalyzer.java | 3 --- .../execution/BaseDataDefinitionTaskTest.java | 2 ++ .../io/trino/sql/analyzer/TestAnalyzer.java | 6 ++++++ .../sql/planner/TestMaterializedViews.java | 4 ++++ .../io/trino/sql/query/TestColumnMask.java | 3 +++ .../io/trino/testing/TestTestingMetadata.java | 1 + core/trino-spi/pom.xml | 5 +++++ .../spi/connector/ConnectorCapabilities.java | 1 + .../ConnectorMaterializedViewDefinition.java | 18 +++++++++++++++++- .../iceberg/catalog/AbstractTrinoCatalog.java | 1 + .../catalog/glue/TrinoGlueCatalog.java | 1 + .../iceberg/catalog/hms/TrinoHiveCatalog.java | 1 + .../iceberg/BaseIcebergConnectorTest.java | 1 + .../iceberg/catalog/BaseTrinoCatalogTest.java | 1 + ...TestTrinoHiveCatalogWithFileMetastore.java | 1 + .../catalog/glue/TestTrinoGlueCatalog.java | 1 + ...TestTrinoHiveCatalogWithHiveMetastore.java | 1 + .../lakehouse/TestLakehouseConnectorTest.java | 1 + .../io/trino/testing/BaseConnectorTest.java | 19 +++++++++++++++++++ .../testing/TestingConnectorBehavior.java | 1 + .../execution/TestEventListenerBasic.java | 2 ++ .../io/trino/execution/TestGrantOnTable.java | 1 + .../TestRefreshMaterializedView.java | 1 + .../io/trino/security/TestAccessControl.java | 1 + .../io/trino/tests/TestMockConnector.java | 1 + 28 files changed, 97 insertions(+), 4 deletions(-) diff --git a/core/trino-main/src/main/java/io/trino/execution/CreateMaterializedViewTask.java b/core/trino-main/src/main/java/io/trino/execution/CreateMaterializedViewTask.java index 7fe0452ee37..5ea8ad61bde 100644 --- a/core/trino-main/src/main/java/io/trino/execution/CreateMaterializedViewTask.java +++ b/core/trino-main/src/main/java/io/trino/execution/CreateMaterializedViewTask.java @@ -27,6 +27,7 @@ import io.trino.metadata.ViewColumn; import io.trino.security.AccessControl; import io.trino.spi.TrinoException; +import io.trino.spi.connector.ConnectorMaterializedViewDefinition.WhenStaleBehavior; import io.trino.spi.type.Type; import io.trino.sql.PlannerContext; import io.trino.sql.analyzer.Analysis; @@ -55,6 +56,7 @@ import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED; import static io.trino.spi.StandardErrorCode.TYPE_MISMATCH; import static io.trino.spi.connector.ConnectorCapabilities.MATERIALIZED_VIEW_GRACE_PERIOD; +import static io.trino.spi.connector.ConnectorCapabilities.MATERIALIZED_VIEW_WHEN_STALE_BEHAVIOR; import static io.trino.sql.SqlFormatterUtil.getFormattedSql; import static io.trino.sql.analyzer.ConstantEvaluator.evaluateConstant; import static io.trino.sql.analyzer.SemanticExceptions.semanticException; @@ -156,12 +158,21 @@ Analysis executeInternal( return Duration.ofMillis(milliseconds); }); + Optional whenStale = statement.getWhenStaleBehavior() + .map(_ -> { + if (!plannerContext.getMetadata().getConnectorCapabilities(session, catalogHandle).contains(MATERIALIZED_VIEW_WHEN_STALE_BEHAVIOR)) { + throw semanticException(NOT_SUPPORTED, statement, "Catalog '%s' does not support WHEN STALE", catalogName); + } + throw semanticException(NOT_SUPPORTED, statement, "WHEN STALE is not supported yet"); + }); + MaterializedViewDefinition definition = new MaterializedViewDefinition( sql, session.getCatalog(), session.getSchema(), columns, gracePeriod, + whenStale, statement.getComment(), session.getIdentity(), session.getPath().getPath().stream() diff --git a/core/trino-main/src/main/java/io/trino/metadata/MaterializedViewDefinition.java b/core/trino-main/src/main/java/io/trino/metadata/MaterializedViewDefinition.java index 7e0f867ffa9..03670d7f8bc 100644 --- a/core/trino-main/src/main/java/io/trino/metadata/MaterializedViewDefinition.java +++ b/core/trino-main/src/main/java/io/trino/metadata/MaterializedViewDefinition.java @@ -16,6 +16,7 @@ import io.trino.spi.connector.CatalogSchemaName; import io.trino.spi.connector.CatalogSchemaTableName; import io.trino.spi.connector.ConnectorMaterializedViewDefinition; +import io.trino.spi.connector.ConnectorMaterializedViewDefinition.WhenStaleBehavior; import io.trino.spi.security.Identity; import java.time.Duration; @@ -31,6 +32,7 @@ public class MaterializedViewDefinition extends ViewDefinition { private final Optional gracePeriod; + private final Optional whenStaleBehavior; private final Optional storageTable; public MaterializedViewDefinition( @@ -39,6 +41,7 @@ public MaterializedViewDefinition( Optional schema, List columns, Optional gracePeriod, + Optional whenStaleBehavior, Optional comment, Identity owner, List path, @@ -47,6 +50,7 @@ public MaterializedViewDefinition( super(originalSql, catalog, schema, columns, comment, Optional.of(owner), path); this.gracePeriod = requireNonNull(gracePeriod, "gracePeriod is null"); checkArgument(gracePeriod.isEmpty() || !gracePeriod.get().isNegative(), "gracePeriod cannot be negative: %s", gracePeriod); + this.whenStaleBehavior = requireNonNull(whenStaleBehavior, "whenStaleBehavior is null"); this.storageTable = requireNonNull(storageTable, "storageTable is null"); } @@ -55,6 +59,11 @@ public Optional getGracePeriod() return gracePeriod; } + public Optional getWhenStaleBehavior() + { + return whenStaleBehavior; + } + public Optional getStorageTable() { return storageTable; @@ -71,6 +80,7 @@ public ConnectorMaterializedViewDefinition toConnectorMaterializedViewDefinition .map(column -> new ConnectorMaterializedViewDefinition.Column(column.name(), column.type(), column.comment())) .collect(toImmutableList()), getGracePeriod(), + whenStaleBehavior, getComment(), getRunAsIdentity().map(Identity::getUser), getPath()); @@ -85,6 +95,7 @@ public String toString() .add("schema", getSchema().orElse(null)) .add("columns", getColumns()) .add("gracePeriod", gracePeriod.orElse(null)) + .add("whenStaleBehavior", whenStaleBehavior.orElse(null)) .add("comment", getComment().orElse(null)) .add("runAsIdentity", getRunAsIdentity()) .add("path", getPath()) 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 a3c272ec9c6..d71098152ce 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 @@ -1827,6 +1827,7 @@ private static MaterializedViewDefinition createMaterializedViewDefinition(Conne .map(column -> new ViewColumn(column.getName(), column.getType(), Optional.empty())) .collect(toImmutableList()), view.getGracePeriod(), + view.getWhenStaleBehavior(), view.getComment(), runAsIdentity, view.getPath(), 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 9906babc4da..858a828bb12 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 @@ -1479,9 +1479,6 @@ protected Scope visitCreateMaterializedView(CreateMaterializedView node, Optiona throw semanticException(NOT_SUPPORTED, node, "'CREATE OR REPLACE' and 'IF NOT EXISTS' clauses can not be used together"); } node.getGracePeriod().ifPresent(gracePeriod -> analyzeExpression(gracePeriod, Scope.create())); - if (node.getWhenStaleBehavior().isPresent()) { - throw new TrinoException(NOT_SUPPORTED, "WHEN STALE is not supported yet"); - } // analyze the query that creates the view StatementAnalyzer analyzer = statementAnalyzerFactory.createStatementAnalyzer(analysis, session, warningCollector, CorrelationSupport.ALLOWED); 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 ecc1b678e8c..752ce28fda1 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 @@ -205,6 +205,7 @@ protected MaterializedViewDefinition someMaterializedView(String sql, List columnName.equals(currentViewColumn.name()) ? new ViewColumn(currentViewColumn.name(), currentViewColumn.type(), comment) : currentViewColumn) .collect(toImmutableList()), view.getGracePeriod(), + view.getWhenStaleBehavior(), view.getComment(), view.getRunAsIdentity().get(), view.getPath(), diff --git a/core/trino-main/src/test/java/io/trino/sql/analyzer/TestAnalyzer.java b/core/trino-main/src/test/java/io/trino/sql/analyzer/TestAnalyzer.java index ff6e6d9a85f..f043bcb366e 100644 --- a/core/trino-main/src/test/java/io/trino/sql/analyzer/TestAnalyzer.java +++ b/core/trino-main/src/test/java/io/trino/sql/analyzer/TestAnalyzer.java @@ -7889,6 +7889,7 @@ public void setup() Optional.of("s1"), ImmutableList.of(new ViewColumn("a", BIGINT.getTypeId(), Optional.empty())), Optional.of(Duration.ZERO), + Optional.empty(), Optional.of("comment"), Identity.ofUser("user"), ImmutableList.of(), @@ -8019,6 +8020,7 @@ public void setup() ImmutableList.of(new ViewColumn("a", BIGINT.getTypeId(), Optional.empty())), Optional.of(Duration.ZERO), Optional.empty(), + Optional.empty(), Identity.ofUser("some user"), ImmutableList.of(), Optional.of(new CatalogSchemaTableName(TPCH_CATALOG, "s1", "t1"))), @@ -8073,6 +8075,7 @@ public void setup() ImmutableList.of(new ViewColumn("a", BIGINT.getTypeId(), Optional.empty()), new ViewColumn("b", BIGINT.getTypeId(), Optional.empty())), Optional.empty(), Optional.empty(), + Optional.empty(), Identity.ofUser("some user"), ImmutableList.of(), // t3 has a, b column and hidden column x @@ -8093,6 +8096,7 @@ public void setup() ImmutableList.of(new ViewColumn("a", BIGINT.getTypeId(), Optional.empty())), Optional.empty(), Optional.empty(), + Optional.empty(), Identity.ofUser("some user"), ImmutableList.of(), Optional.of(new CatalogSchemaTableName(TPCH_CATALOG, "s1", "t2"))), @@ -8112,6 +8116,7 @@ public void setup() ImmutableList.of(new ViewColumn("a", BIGINT.getTypeId(), Optional.empty()), new ViewColumn("c", BIGINT.getTypeId(), Optional.empty())), Optional.empty(), Optional.empty(), + Optional.empty(), Identity.ofUser("some user"), ImmutableList.of(), Optional.of(new CatalogSchemaTableName(TPCH_CATALOG, "s1", "t2"))), @@ -8131,6 +8136,7 @@ public void setup() ImmutableList.of(new ViewColumn("a", BIGINT.getTypeId(), Optional.empty()), new ViewColumn("b", RowType.anonymousRow(TINYINT).getTypeId(), Optional.empty())), Optional.empty(), Optional.empty(), + Optional.empty(), Identity.ofUser("some user"), ImmutableList.of(), Optional.of(new CatalogSchemaTableName(TPCH_CATALOG, "s1", "t2"))), diff --git a/core/trino-main/src/test/java/io/trino/sql/planner/TestMaterializedViews.java b/core/trino-main/src/test/java/io/trino/sql/planner/TestMaterializedViews.java index 224d5a4ed77..0140c1a6cc4 100644 --- a/core/trino-main/src/test/java/io/trino/sql/planner/TestMaterializedViews.java +++ b/core/trino-main/src/test/java/io/trino/sql/planner/TestMaterializedViews.java @@ -186,6 +186,7 @@ protected PlanTester createPlanTester() ImmutableList.of(new ViewColumn("a", BIGINT.getTypeId(), Optional.empty()), new ViewColumn("b", BIGINT.getTypeId(), Optional.empty())), Optional.of(STALE_MV_STALENESS.plusHours(1)), Optional.empty(), + Optional.empty(), Identity.ofUser("some user"), ImmutableList.of(), Optional.of(new CatalogSchemaTableName(TEST_CATALOG_NAME, SCHEMA, "storage_table"))); @@ -220,6 +221,7 @@ protected PlanTester createPlanTester() ImmutableList.of(new ViewColumn("a", BIGINT.getTypeId(), Optional.empty()), new ViewColumn("b", BIGINT.getTypeId(), Optional.empty())), Optional.empty(), Optional.empty(), + Optional.empty(), Identity.ofUser("some user"), ImmutableList.of(), Optional.of(new CatalogSchemaTableName(TEST_CATALOG_NAME, SCHEMA, "storage_table_with_casts"))); @@ -254,6 +256,7 @@ protected PlanTester createPlanTester() ImmutableList.of(new ViewColumn("id", BIGINT.getTypeId(), Optional.empty()), new ViewColumn("ts", timestampWithTimezone3.getTypeId(), Optional.empty())), Optional.empty(), Optional.empty(), + Optional.empty(), Identity.ofUser("some user"), ImmutableList.of(), Optional.of(new CatalogSchemaTableName(TEST_CATALOG_NAME, SCHEMA, "timestamp_test_storage"))); @@ -285,6 +288,7 @@ private void createMaterializedView(String materializedViewName, String query) ImmutableList.of(new ViewColumn("a", BIGINT.getTypeId(), Optional.empty()), new ViewColumn("b", BIGINT.getTypeId(), Optional.empty())), Optional.of(STALE_MV_STALENESS.plusHours(1)), Optional.empty(), + Optional.empty(), Identity.ofUser("some user"), ImmutableList.of(), Optional.of(new CatalogSchemaTableName(TEST_CATALOG_NAME, SCHEMA, "storage_table"))); diff --git a/core/trino-main/src/test/java/io/trino/sql/query/TestColumnMask.java b/core/trino-main/src/test/java/io/trino/sql/query/TestColumnMask.java index 560faad79a9..a9e0ecb7da7 100644 --- a/core/trino-main/src/test/java/io/trino/sql/query/TestColumnMask.java +++ b/core/trino-main/src/test/java/io/trino/sql/query/TestColumnMask.java @@ -133,6 +133,7 @@ public TestColumnMask() new ConnectorMaterializedViewDefinition.Column("comment", VarcharType.createVarcharType(152).getTypeId(), Optional.empty())), Optional.of(Duration.ZERO), Optional.empty(), + Optional.empty(), Optional.of(VIEW_OWNER), ImmutableList.of()); @@ -148,6 +149,7 @@ public TestColumnMask() new ConnectorMaterializedViewDefinition.Column("comment", VarcharType.createVarcharType(152).getTypeId(), Optional.empty())), Optional.of(Duration.ZERO), Optional.empty(), + Optional.empty(), Optional.of(VIEW_OWNER), ImmutableList.of()); @@ -163,6 +165,7 @@ public TestColumnMask() new ConnectorMaterializedViewDefinition.Column("comment", VarcharType.createVarcharType(152).getTypeId(), Optional.empty())), Optional.of(Duration.ZERO), Optional.empty(), + Optional.empty(), Optional.of(VIEW_OWNER), ImmutableList.of()); diff --git a/core/trino-main/src/test/java/io/trino/testing/TestTestingMetadata.java b/core/trino-main/src/test/java/io/trino/testing/TestTestingMetadata.java index d188741497d..26d3ebca70e 100644 --- a/core/trino-main/src/test/java/io/trino/testing/TestTestingMetadata.java +++ b/core/trino-main/src/test/java/io/trino/testing/TestTestingMetadata.java @@ -61,6 +61,7 @@ private static ConnectorMaterializedViewDefinition someMaterializedView() ImmutableList.of(new Column("test", BIGINT.getTypeId(), Optional.empty())), Optional.of(Duration.ZERO), Optional.empty(), + Optional.empty(), Optional.of("owner"), ImmutableList.of()); } diff --git a/core/trino-spi/pom.xml b/core/trino-spi/pom.xml index 14ab18f30ce..d7e7ad4eeca 100644 --- a/core/trino-spi/pom.xml +++ b/core/trino-spi/pom.xml @@ -297,6 +297,11 @@ method void io.trino.spi.block.PageBuilderStatus::<init>() Method is unnecessary + + java.method.numberOfParametersChanged + method void io.trino.spi.connector.ConnectorMaterializedViewDefinition::<init>(java.lang.String, java.util.Optional<io.trino.spi.connector.CatalogSchemaTableName>, java.util.Optional<java.lang.String>, java.util.Optional<java.lang.String>, java.util.List<io.trino.spi.connector.ConnectorMaterializedViewDefinition.Column>, java.util.Optional<java.time.Duration>, java.util.Optional<java.lang.String>, java.util.Optional<java.lang.String>, java.util.List<io.trino.spi.connector.CatalogSchemaName>) + method void io.trino.spi.connector.ConnectorMaterializedViewDefinition::<init>(java.lang.String, java.util.Optional<io.trino.spi.connector.CatalogSchemaTableName>, java.util.Optional<java.lang.String>, java.util.Optional<java.lang.String>, java.util.List<io.trino.spi.connector.ConnectorMaterializedViewDefinition.Column>, java.util.Optional<java.time.Duration>, java.util.Optional<io.trino.spi.connector.ConnectorMaterializedViewDefinition.WhenStaleBehavior>, java.util.Optional<java.lang.String>, java.util.Optional<java.lang.String>, java.util.List<io.trino.spi.connector.CatalogSchemaName>) + diff --git a/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorCapabilities.java b/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorCapabilities.java index faec15da9b0..5e50af6ec33 100644 --- a/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorCapabilities.java +++ b/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorCapabilities.java @@ -18,4 +18,5 @@ public enum ConnectorCapabilities DEFAULT_COLUMN_VALUE, NOT_NULL_COLUMN_CONSTRAINT, MATERIALIZED_VIEW_GRACE_PERIOD, + MATERIALIZED_VIEW_WHEN_STALE_BEHAVIOR, } diff --git a/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorMaterializedViewDefinition.java b/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorMaterializedViewDefinition.java index d7ebd0097f4..559a48e12d5 100644 --- a/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorMaterializedViewDefinition.java +++ b/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorMaterializedViewDefinition.java @@ -27,12 +27,19 @@ public class ConnectorMaterializedViewDefinition { + public enum WhenStaleBehavior + { + INLINE, + FAIL, + } + private final String originalSql; private final Optional storageTable; private final Optional catalog; private final Optional schema; private final List columns; private final Optional gracePeriod; + private final Optional whenStaleBehavior; private final Optional comment; private final Optional owner; private final List path; @@ -44,6 +51,7 @@ public ConnectorMaterializedViewDefinition( Optional schema, List columns, Optional gracePeriod, + Optional whenStaleBehavior, Optional comment, Optional owner, List path) @@ -55,6 +63,7 @@ public ConnectorMaterializedViewDefinition( this.columns = List.copyOf(requireNonNull(columns, "columns is null")); checkArgument(gracePeriod.isEmpty() || !gracePeriod.get().isNegative(), "gracePeriod cannot be negative: %s", gracePeriod); this.gracePeriod = gracePeriod; + this.whenStaleBehavior = requireNonNull(whenStaleBehavior, "whenStaleBehavior is null"); this.comment = requireNonNull(comment, "comment is null"); this.owner = requireNonNull(owner, "owner is null"); this.path = List.copyOf(path); @@ -97,6 +106,11 @@ public Optional getGracePeriod() return gracePeriod; } + public Optional getWhenStaleBehavior() + { + return whenStaleBehavior; + } + public Optional getComment() { return comment; @@ -122,6 +136,7 @@ public String toString() schema.ifPresent(value -> joiner.add("schema=" + value)); joiner.add("columns=" + columns); gracePeriod.ifPresent(value -> joiner.add("gracePeriod=" + gracePeriod)); + whenStaleBehavior.ifPresent(value -> joiner.add("whenStaleBehavior=" + value.name())); comment.ifPresent(value -> joiner.add("comment=" + value)); joiner.add("owner=" + owner); joiner.add(path.stream().map(CatalogSchemaName::toString).collect(joining(", ", "path=(", ")"))); @@ -144,6 +159,7 @@ public boolean equals(Object o) Objects.equals(schema, that.schema) && Objects.equals(columns, that.columns) && Objects.equals(gracePeriod, that.gracePeriod) && + Objects.equals(whenStaleBehavior, that.whenStaleBehavior) && Objects.equals(comment, that.comment) && Objects.equals(owner, that.owner) && Objects.equals(path, that.path); @@ -152,7 +168,7 @@ public boolean equals(Object o) @Override public int hashCode() { - return Objects.hash(originalSql, storageTable, catalog, schema, columns, gracePeriod, comment, owner, path); + return Objects.hash(originalSql, storageTable, catalog, schema, columns, gracePeriod, whenStaleBehavior, comment, owner, path); } public static final class Column diff --git a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/AbstractTrinoCatalog.java b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/AbstractTrinoCatalog.java index 4268acc934d..6bbbd92bb2f 100644 --- a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/AbstractTrinoCatalog.java +++ b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/AbstractTrinoCatalog.java @@ -466,6 +466,7 @@ protected ConnectorMaterializedViewDefinition getMaterializedViewDefinition( definition.schema(), toSpiMaterializedViewColumns(definition.columns()), definition.gracePeriod(), + Optional.empty(), definition.comment(), owner, definition.path()); diff --git a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/glue/TrinoGlueCatalog.java b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/glue/TrinoGlueCatalog.java index 85d761d1062..f6045e07261 100644 --- a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/glue/TrinoGlueCatalog.java +++ b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/glue/TrinoGlueCatalog.java @@ -1232,6 +1232,7 @@ public void updateMaterializedViewColumnComment(ConnectorSession session, Schema .map(currentViewColumn -> Objects.equals(columnName, currentViewColumn.getName()) ? new ConnectorMaterializedViewDefinition.Column(currentViewColumn.getName(), currentViewColumn.getType(), comment) : currentViewColumn) .collect(toImmutableList()), definition.getGracePeriod(), + definition.getWhenStaleBehavior(), definition.getComment(), definition.getOwner(), definition.getPath()); diff --git a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/hms/TrinoHiveCatalog.java b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/hms/TrinoHiveCatalog.java index 06460d638b7..34f9d0d32cf 100644 --- a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/hms/TrinoHiveCatalog.java +++ b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/hms/TrinoHiveCatalog.java @@ -694,6 +694,7 @@ public void updateMaterializedViewColumnComment(ConnectorSession session, Schema : currentViewColumn) .collect(toImmutableList()), definition.getGracePeriod(), + definition.getWhenStaleBehavior(), definition.getComment(), definition.getOwner(), definition.getPath()); 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 608bdf00f2a..0e4768f4fbd 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 @@ -280,6 +280,7 @@ protected boolean hasBehavior(TestingConnectorBehavior connectorBehavior) SUPPORTS_LIMIT_PUSHDOWN, SUPPORTS_REFRESH_VIEW, SUPPORTS_RENAME_MATERIALIZED_VIEW_ACROSS_SCHEMAS, + SUPPORTS_CREATE_MATERIALIZED_VIEW_WHEN_STALE, SUPPORTS_TOPN_PUSHDOWN -> false; default -> super.hasBehavior(connectorBehavior); }; diff --git a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/BaseTrinoCatalogTest.java b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/BaseTrinoCatalogTest.java index 212d9c28239..6288a52f6b7 100644 --- a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/BaseTrinoCatalogTest.java +++ b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/BaseTrinoCatalogTest.java @@ -647,6 +647,7 @@ private static ConnectorMaterializedViewDefinition someMaterializedView() ImmutableList.of(new ConnectorMaterializedViewDefinition.Column("test", BIGINT.getTypeId(), Optional.empty())), Optional.of(Duration.ZERO), Optional.empty(), + Optional.empty(), Optional.of("owner"), ImmutableList.of()); } diff --git a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/file/TestTrinoHiveCatalogWithFileMetastore.java b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/file/TestTrinoHiveCatalogWithFileMetastore.java index d1147eed5fa..9c9329608e7 100644 --- a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/file/TestTrinoHiveCatalogWithFileMetastore.java +++ b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/file/TestTrinoHiveCatalogWithFileMetastore.java @@ -153,6 +153,7 @@ private void testDropMaterializedView(boolean useUniqueTableLocations) Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), ImmutableList.of()), ImmutableMap.of(FILE_FORMAT_PROPERTY, PARQUET, FORMAT_VERSION_PROPERTY, 1), false, diff --git a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/glue/TestTrinoGlueCatalog.java b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/glue/TestTrinoGlueCatalog.java index 7cfacf22d94..b2c71e66174 100644 --- a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/glue/TestTrinoGlueCatalog.java +++ b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/glue/TestTrinoGlueCatalog.java @@ -186,6 +186,7 @@ public void testCreateMaterializedViewWithSystemSecurity() ImmutableList.of(new ConnectorMaterializedViewDefinition.Column("col1", INTEGER.getTypeId(), Optional.empty())), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.of("test_owner"), ImmutableList.of()), ImmutableMap.of(FILE_FORMAT_PROPERTY, PARQUET, FORMAT_VERSION_PROPERTY, 1), diff --git a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/hms/TestTrinoHiveCatalogWithHiveMetastore.java b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/hms/TestTrinoHiveCatalogWithHiveMetastore.java index 2baf83cd2aa..2974187cc08 100644 --- a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/hms/TestTrinoHiveCatalogWithHiveMetastore.java +++ b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/hms/TestTrinoHiveCatalogWithHiveMetastore.java @@ -215,6 +215,7 @@ public void testCreateMaterializedView() Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), ImmutableList.of()), ImmutableMap.of(FILE_FORMAT_PROPERTY, PARQUET, FORMAT_VERSION_PROPERTY, 1), false, 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 465790ae611..b77ec9d1a14 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 @@ -127,6 +127,7 @@ protected boolean hasBehavior(TestingConnectorBehavior connectorBehavior) SUPPORTS_LIMIT_PUSHDOWN, SUPPORTS_REFRESH_VIEW, SUPPORTS_RENAME_MATERIALIZED_VIEW_ACROSS_SCHEMAS, + SUPPORTS_CREATE_MATERIALIZED_VIEW_WHEN_STALE, SUPPORTS_TOPN_PUSHDOWN -> false; default -> super.hasBehavior(connectorBehavior); }; 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 9ef397e19ee..4651cf1490b 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 @@ -127,6 +127,7 @@ import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_CREATE_FUNCTION; import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_CREATE_MATERIALIZED_VIEW; import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_CREATE_MATERIALIZED_VIEW_GRACE_PERIOD; +import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_CREATE_MATERIALIZED_VIEW_WHEN_STALE; import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_CREATE_OR_REPLACE_TABLE; import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_CREATE_SCHEMA; import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_CREATE_TABLE; @@ -1596,6 +1597,24 @@ public void testMaterializedViewGracePeriod() } } + @Test + public void testMaterializedViewWhenStale() + { + skipTestUnless(hasBehavior(SUPPORTS_CREATE_MATERIALIZED_VIEW)); + + String catalog = getSession().getCatalog().orElseThrow(); + String viewName = "test_mv_when_stale_" + randomNameSuffix(); + + if (!hasBehavior(SUPPORTS_CREATE_MATERIALIZED_VIEW_WHEN_STALE)) { + assertQueryFails( + "CREATE MATERIALIZED VIEW " + viewName + " WHEN STALE FAIL AS SELECT * FROM nation", + "line 1:1: Catalog '%s' does not support WHEN STALE".formatted(catalog)); + return; + } + + throw new UnsupportedOperationException("Not implemented"); + } + @Test public void testFederatedMaterializedView() { 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 aae3e3c4922..ce14457e977 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 @@ -108,6 +108,7 @@ public enum TestingConnectorBehavior SUPPORTS_CREATE_MATERIALIZED_VIEW, SUPPORTS_CREATE_MATERIALIZED_VIEW_GRACE_PERIOD(SUPPORTS_CREATE_MATERIALIZED_VIEW), + SUPPORTS_CREATE_MATERIALIZED_VIEW_WHEN_STALE(SUPPORTS_CREATE_MATERIALIZED_VIEW), SUPPORTS_CREATE_FEDERATED_MATERIALIZED_VIEW(SUPPORTS_CREATE_MATERIALIZED_VIEW), // i.e. an MV that spans catalogs SUPPORTS_MATERIALIZED_VIEW_FRESHNESS_FROM_BASE_TABLES(SUPPORTS_CREATE_MATERIALIZED_VIEW), SUPPORTS_RENAME_MATERIALIZED_VIEW(SUPPORTS_CREATE_MATERIALIZED_VIEW), diff --git a/testing/trino-tests/src/test/java/io/trino/execution/TestEventListenerBasic.java b/testing/trino-tests/src/test/java/io/trino/execution/TestEventListenerBasic.java index fad98bed11c..944c36036b1 100644 --- a/testing/trino-tests/src/test/java/io/trino/execution/TestEventListenerBasic.java +++ b/testing/trino-tests/src/test/java/io/trino/execution/TestEventListenerBasic.java @@ -222,6 +222,7 @@ public Iterable getConnectorFactories() ImmutableList.of(new Column("test_column", BIGINT.getTypeId(), Optional.empty())), Optional.of(Duration.ZERO), Optional.empty(), + Optional.empty(), Optional.of("alice"), ImmutableList.of()); ConnectorMaterializedViewDefinition definitionFresh = new ConnectorMaterializedViewDefinition( @@ -235,6 +236,7 @@ public Iterable getConnectorFactories() .collect(toImmutableList()), Optional.of(Duration.ofDays(1)), Optional.empty(), + Optional.empty(), Optional.of("alice"), ImmutableList.of()); return ImmutableMap.of( diff --git a/testing/trino-tests/src/test/java/io/trino/execution/TestGrantOnTable.java b/testing/trino-tests/src/test/java/io/trino/execution/TestGrantOnTable.java index 9018ec5c61b..297330cc6b4 100644 --- a/testing/trino-tests/src/test/java/io/trino/execution/TestGrantOnTable.java +++ b/testing/trino-tests/src/test/java/io/trino/execution/TestGrantOnTable.java @@ -99,6 +99,7 @@ materializedView, new ConnectorMaterializedViewDefinition( ImmutableList.of(new ConnectorMaterializedViewDefinition.Column("test_column", BIGINT.getTypeId(), Optional.empty())), Optional.of(Duration.ZERO), Optional.empty(), + Optional.empty(), Optional.of("alice"), ImmutableList.of()))) .build(); diff --git a/testing/trino-tests/src/test/java/io/trino/execution/TestRefreshMaterializedView.java b/testing/trino-tests/src/test/java/io/trino/execution/TestRefreshMaterializedView.java index 61f7911c4ee..45678358d84 100644 --- a/testing/trino-tests/src/test/java/io/trino/execution/TestRefreshMaterializedView.java +++ b/testing/trino-tests/src/test/java/io/trino/execution/TestRefreshMaterializedView.java @@ -104,6 +104,7 @@ protected QueryRunner createQueryRunner() ImmutableList.of(new ConnectorMaterializedViewDefinition.Column("nationkey", BIGINT.getTypeId(), Optional.empty())), Optional.of(Duration.ZERO), Optional.empty(), + Optional.empty(), Optional.of("alice"), ImmutableList.of()))) .withDelegateMaterializedViewRefreshToConnector((connectorSession, schemaTableName) -> true) diff --git a/testing/trino-tests/src/test/java/io/trino/security/TestAccessControl.java b/testing/trino-tests/src/test/java/io/trino/security/TestAccessControl.java index 8724a167229..9ed632c5338 100644 --- a/testing/trino-tests/src/test/java/io/trino/security/TestAccessControl.java +++ b/testing/trino-tests/src/test/java/io/trino/security/TestAccessControl.java @@ -219,6 +219,7 @@ public Map apply(Connector Optional.empty(), ImmutableList.of(new ConnectorMaterializedViewDefinition.Column("test", BIGINT.getTypeId(), Optional.empty())), Optional.of(Duration.ZERO), + Optional.empty(), Optional.of("comment"), Optional.of("owner"), ImmutableList.of()); diff --git a/testing/trino-tests/src/test/java/io/trino/tests/TestMockConnector.java b/testing/trino-tests/src/test/java/io/trino/tests/TestMockConnector.java index 7dc57c38fa7..48693fe345a 100644 --- a/testing/trino-tests/src/test/java/io/trino/tests/TestMockConnector.java +++ b/testing/trino-tests/src/test/java/io/trino/tests/TestMockConnector.java @@ -105,6 +105,7 @@ protected QueryRunner createQueryRunner() ImmutableList.of(new Column("nationkey", BIGINT.getTypeId(), Optional.empty())), Optional.of(Duration.ZERO), Optional.empty(), + Optional.empty(), Optional.of("alice"), ImmutableList.of()))) .withData(schemaTableName -> { From 8d70622469b2163d55b4d5352dd4135669ed3422 Mon Sep 17 00:00:00 2001 From: Piotr Rzysko Date: Tue, 18 Nov 2025 07:49:24 +0100 Subject: [PATCH 3/3] Support WHEN STALE in analyzer Behavior: - `FAIL` - If the MV is stale, queries referencing it will fail. Analysis of the underlying query is never performed while querying the MV. - `INLINE` - Preserves the current behavior. Even if the MV is stale, it is expanded like a logical view, and the underlying query is analyzed. Motivation: - Avoid analyzing the underlying query every time, which can be costly when it references many tables. - Allow using fresh MVs even when some data sources are temporarily unavailable. --- .../execution/CreateMaterializedViewTask.java | 12 +- .../trino/sql/analyzer/StatementAnalyzer.java | 65 ++++-- .../trino/sql/rewrite/ShowQueriesRewrite.java | 13 +- .../io/trino/sql/analyzer/TestAnalyzer.java | 79 +++++-- .../sql/planner/TestMaterializedViews.java | 133 ++++++++---- .../plugin/iceberg/IcebergConnector.java | 4 +- .../IcebergMaterializedViewDefinition.java | 5 + .../iceberg/catalog/AbstractTrinoCatalog.java | 2 +- .../iceberg/BaseIcebergConnectorTest.java | 1 - .../plugin/lakehouse/LakehouseConnector.java | 3 +- .../lakehouse/TestLakehouseConnectorTest.java | 1 - .../io/trino/testing/BaseConnectorTest.java | 192 +++++++++++++++++- 12 files changed, 423 insertions(+), 87 deletions(-) diff --git a/core/trino-main/src/main/java/io/trino/execution/CreateMaterializedViewTask.java b/core/trino-main/src/main/java/io/trino/execution/CreateMaterializedViewTask.java index 5ea8ad61bde..f46e6a27361 100644 --- a/core/trino-main/src/main/java/io/trino/execution/CreateMaterializedViewTask.java +++ b/core/trino-main/src/main/java/io/trino/execution/CreateMaterializedViewTask.java @@ -159,11 +159,11 @@ Analysis executeInternal( }); Optional whenStale = statement.getWhenStaleBehavior() - .map(_ -> { + .map(whenStaleBehavior -> { if (!plannerContext.getMetadata().getConnectorCapabilities(session, catalogHandle).contains(MATERIALIZED_VIEW_WHEN_STALE_BEHAVIOR)) { throw semanticException(NOT_SUPPORTED, statement, "Catalog '%s' does not support WHEN STALE", catalogName); } - throw semanticException(NOT_SUPPORTED, statement, "WHEN STALE is not supported yet"); + return toConnectorWhenStaleBehavior(whenStaleBehavior); }); MaterializedViewDefinition definition = new MaterializedViewDefinition( @@ -193,4 +193,12 @@ Analysis executeInternal( plannerContext.getMetadata().createMaterializedView(session, name, definition, properties, statement.isReplace(), statement.isNotExists()); return analysis; } + + private static WhenStaleBehavior toConnectorWhenStaleBehavior(CreateMaterializedView.WhenStaleBehavior whenStale) + { + return switch (whenStale) { + case INLINE -> WhenStaleBehavior.INLINE; + case FAIL -> WhenStaleBehavior.FAIL; + }; + } } 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 858a828bb12..8b35fbd625b 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 @@ -66,6 +66,7 @@ import io.trino.spi.connector.ColumnHandle; import io.trino.spi.connector.ColumnMetadata; import io.trino.spi.connector.ColumnSchema; +import io.trino.spi.connector.ConnectorMaterializedViewDefinition.WhenStaleBehavior; import io.trino.spi.connector.ConnectorTableMetadata; import io.trino.spi.connector.ConnectorTransactionHandle; import io.trino.spi.connector.MaterializedViewFreshness; @@ -2299,6 +2300,7 @@ protected Scope visitTable(Table table, Optional scope) if (optionalMaterializedView.isPresent()) { MaterializedViewDefinition materializedViewDefinition = optionalMaterializedView.get(); analysis.addEmptyColumnReferencesForTable(accessControl, session.getIdentity(), name); + boolean useLogicalViewSemantics = shouldUseLogicalViewSemantics(materializedViewDefinition); if (isMaterializedViewSufficientlyFresh(session, name, materializedViewDefinition)) { // If materialized view is sufficiently fresh with respect to its grace period, answer the query using the storage table QualifiedName storageName = getMaterializedViewStorageTableName(materializedViewDefinition) @@ -2307,10 +2309,13 @@ protected Scope visitTable(Table table, Optional scope) checkStorageTableNotRedirected(storageTableName); TableHandle tableHandle = metadata.getTableHandle(session, storageTableName) .orElseThrow(() -> semanticException(INVALID_VIEW, table, "Storage table '%s' does not exist", storageTableName)); - return createScopeForMaterializedView(table, name, scope, materializedViewDefinition, Optional.of(tableHandle)); + return createScopeForMaterializedView(table, name, scope, materializedViewDefinition, Optional.of(tableHandle), useLogicalViewSemantics); + } + else if (!useLogicalViewSemantics) { + throw semanticException(VIEW_IS_STALE, table, "Materialized view '%s' is stale", name); } // This is a stale materialized view and should be expanded like a logical view - return createScopeForMaterializedView(table, name, scope, materializedViewDefinition, Optional.empty()); + return createScopeForMaterializedView(table, name, scope, materializedViewDefinition, Optional.empty(), useLogicalViewSemantics); } // This could be a reference to a logical view or a table @@ -2399,6 +2404,15 @@ private boolean isMaterializedViewSufficientlyFresh(Session session, QualifiedOb return staleness.compareTo(gracePeriod) <= 0; } + private static boolean shouldUseLogicalViewSemantics(MaterializedViewDefinition materializedViewDefinition) + { + WhenStaleBehavior whenStale = materializedViewDefinition.getWhenStaleBehavior().orElse(WhenStaleBehavior.INLINE); + return switch (whenStale) { + case WhenStaleBehavior.INLINE -> true; + case WhenStaleBehavior.FAIL -> false; + }; + } + private void checkStorageTableNotRedirected(QualifiedObjectName source) { metadata.getRedirectionAwareTableHandle(session, source).redirectedTableName().ifPresent(name -> { @@ -2543,7 +2557,13 @@ private Scope createScopeForCommonTableExpression(Table table, Optional s return createAndAssignScope(table, scope, fields); } - private Scope createScopeForMaterializedView(Table table, QualifiedObjectName name, Optional scope, MaterializedViewDefinition view, Optional storageTable) + private Scope createScopeForMaterializedView( + Table table, + QualifiedObjectName name, + Optional scope, + MaterializedViewDefinition view, + Optional storageTable, + boolean useLogicalViewSemantics) { return createScopeForView( table, @@ -2556,7 +2576,8 @@ private Scope createScopeForMaterializedView(Table table, QualifiedObjectName na view.getPath(), view.getColumns(), storageTable, - true); + true, + useLogicalViewSemantics); } private Scope createScopeForView(Table table, QualifiedObjectName name, Optional scope, ViewDefinition view) @@ -2571,7 +2592,8 @@ private Scope createScopeForView(Table table, QualifiedObjectName name, Optional view.getPath(), view.getColumns(), Optional.empty(), - false); + false, + true); } private Scope createScopeForView( @@ -2585,7 +2607,8 @@ private Scope createScopeForView( List path, List columns, Optional storageTable, - boolean isMaterializedView) + boolean isMaterializedView, + boolean useLogicalViewSemantics) { Statement statement = analysis.getStatement(); if (statement instanceof CreateView viewStatement) { @@ -2604,18 +2627,27 @@ private Scope createScopeForView( throw semanticException(VIEW_IS_RECURSIVE, table, "View is recursive"); } - Query query = parseView(originalSql, name, table); + if (useLogicalViewSemantics) { + Query query = parseView(originalSql, name, table); - if (!query.getFunctions().isEmpty()) { - throw semanticException(NOT_SUPPORTED, table, "View contains inline function: %s", name); - } + if (!query.getFunctions().isEmpty()) { + throw semanticException(NOT_SUPPORTED, table, "View contains inline function: %s", name); + } - analysis.registerTableForView(table, name, isMaterializedView); - RelationType descriptor = analyzeView(query, name, catalog, schema, owner, path, table); - analysis.unregisterTableForView(); + analysis.registerTableForView(table, name, isMaterializedView); + RelationType descriptor = analyzeView(query, name, catalog, schema, owner, path, table); + analysis.unregisterTableForView(); - checkViewStaleness(columns, descriptor.getVisibleFields(), name, table) - .ifPresent(explanation -> { throw semanticException(VIEW_IS_STALE, table, "View '%s' is stale or in invalid state: %s", name, explanation); }); + checkViewStaleness(columns, descriptor.getVisibleFields(), name, table) + .ifPresent(explanation -> { throw semanticException(VIEW_IS_STALE, table, "View '%s' is stale or in invalid state: %s", name, explanation); }); + + if (storageTable.isEmpty()) { + analysis.registerNamedQuery(table, query); + } + } + else { + checkArgument(storageTable.isPresent(), "A storage table must be present when query analysis is skipped"); + } // Derive the type of the view from the stored definition, not from the analysis of the underlying query. // This is needed in case the underlying table(s) changed and the query in the view now produces types that @@ -2635,9 +2667,6 @@ private Scope createScopeForView( List storageTableFields = analyzeStorageTable(table, viewFields, storageTable.get()); analysis.setMaterializedViewStorageTableFields(table, storageTableFields); } - else { - analysis.registerNamedQuery(table, query); - } Scope accessControlScope = Scope.builder() .withRelationType(RelationId.anonymous(), new RelationType(viewFields)) diff --git a/core/trino-main/src/main/java/io/trino/sql/rewrite/ShowQueriesRewrite.java b/core/trino-main/src/main/java/io/trino/sql/rewrite/ShowQueriesRewrite.java index fba03ce4215..f2a7b928dbd 100644 --- a/core/trino-main/src/main/java/io/trino/sql/rewrite/ShowQueriesRewrite.java +++ b/core/trino-main/src/main/java/io/trino/sql/rewrite/ShowQueriesRewrite.java @@ -40,6 +40,7 @@ import io.trino.metadata.ViewPropertyManager; import io.trino.security.AccessControl; import io.trino.spi.connector.CatalogSchemaName; +import io.trino.spi.connector.ConnectorMaterializedViewDefinition; import io.trino.spi.connector.ConnectorTableMetadata; import io.trino.spi.connector.SchemaTableName; import io.trino.spi.function.FunctionKind; @@ -61,6 +62,7 @@ import io.trino.sql.tree.Cast; import io.trino.sql.tree.ColumnDefinition; import io.trino.sql.tree.CreateMaterializedView; +import io.trino.sql.tree.CreateMaterializedView.WhenStaleBehavior; import io.trino.sql.tree.CreateSchema; import io.trino.sql.tree.CreateTable; import io.trino.sql.tree.CreateView; @@ -562,12 +564,21 @@ private Query showCreateMaterializedView(ShowCreate node) false, false, Optional.empty(), // TODO support GRACE PERIOD - Optional.empty(), // TODO support WHEN STALE + viewDefinition.flatMap(MaterializedViewDefinition::getWhenStaleBehavior) + .map(Visitor::toSqlWhenStaleBehavior), propertyNodes, viewDefinition.get().getComment())).trim(); return singleValueQuery("Create Materialized View", sql); } + private static WhenStaleBehavior toSqlWhenStaleBehavior(ConnectorMaterializedViewDefinition.WhenStaleBehavior whenStale) + { + return switch (whenStale) { + case INLINE -> WhenStaleBehavior.INLINE; + case FAIL -> WhenStaleBehavior.FAIL; + }; + } + private Query showCreateView(ShowCreate node) { QualifiedObjectName objectName = createQualifiedObjectName(session, node, node.getName()); diff --git a/core/trino-main/src/test/java/io/trino/sql/analyzer/TestAnalyzer.java b/core/trino-main/src/test/java/io/trino/sql/analyzer/TestAnalyzer.java index f043bcb366e..2ae68c5d70d 100644 --- a/core/trino-main/src/test/java/io/trino/sql/analyzer/TestAnalyzer.java +++ b/core/trino-main/src/test/java/io/trino/sql/analyzer/TestAnalyzer.java @@ -73,6 +73,7 @@ import io.trino.spi.connector.CatalogSchemaTableName; import io.trino.spi.connector.ColumnMetadata; import io.trino.spi.connector.Connector; +import io.trino.spi.connector.ConnectorMaterializedViewDefinition.WhenStaleBehavior; import io.trino.spi.connector.ConnectorMetadata; import io.trino.spi.connector.ConnectorSession; import io.trino.spi.connector.ConnectorTableMetadata; @@ -112,6 +113,7 @@ import java.time.Duration; import java.util.List; +import java.util.Locale; import java.util.Optional; import java.util.function.Consumer; @@ -5801,13 +5803,20 @@ public void testAnalyzeFreshMaterializedView() @Test public void testAnalyzeInvalidFreshMaterializedView() { - assertFails("SELECT * FROM fresh_materialized_view_mismatched_column_count") + testAnalyzeInvalidFreshMaterializedView(Optional.empty()); + testAnalyzeInvalidFreshMaterializedView(Optional.of(WhenStaleBehavior.FAIL)); + } + + private void testAnalyzeInvalidFreshMaterializedView(Optional whenStaleBehavior) + { + String suffix = resolveMaterializedViewNameSuffix(whenStaleBehavior); + assertFails("SELECT * FROM fresh_materialized_view_mismatched_column_count" + suffix) .hasErrorCode(INVALID_VIEW) .hasMessage("line 1:15: storage table column count (2) does not match column count derived from the materialized view query analysis (1)"); - assertFails("SELECT * FROM fresh_materialized_view_mismatched_column_name") + assertFails("SELECT * FROM fresh_materialized_view_mismatched_column_name" + suffix) .hasErrorCode(INVALID_VIEW) .hasMessage("line 1:15: column [b] of type bigint projected from storage table at position 1 has a different name from column [c] of type bigint stored in materialized view definition"); - assertFails("SELECT * FROM fresh_materialized_view_mismatched_column_type") + assertFails("SELECT * FROM fresh_materialized_view_mismatched_column_type" + suffix) .hasErrorCode(INVALID_VIEW) .hasMessage("line 1:15: cannot cast column [b] of type bigint projected from storage table at position 1 into column [b] of type row(tinyint) stored in view definition"); } @@ -5815,22 +5824,47 @@ public void testAnalyzeInvalidFreshMaterializedView() @Test public void testAnalyzeMaterializedViewWithAccessControl() { + testAnalyzeMaterializedViewWithAccessControl(Optional.empty()); + testAnalyzeMaterializedViewWithAccessControl(Optional.of(WhenStaleBehavior.FAIL)); + } + + private void testAnalyzeMaterializedViewWithAccessControl(Optional whenStaleBehavior) + { + String suffix = resolveMaterializedViewNameSuffix(whenStaleBehavior); + TestingAccessControlManager accessControlManager = new TestingAccessControlManager(transactionManager, emptyEventListenerManager(), new SecretsResolver(ImmutableMap.of())); accessControlManager.setSystemAccessControls(List.of(AllowAllSystemAccessControl.INSTANCE)); - analyze(CLIENT_SESSION, "SELECT * FROM fresh_materialized_view", accessControlManager); + analyze(CLIENT_SESSION, "SELECT * FROM fresh_materialized_view" + suffix, accessControlManager); // materialized view analysis should succeed even if access to storage table is denied when querying the table directly accessControlManager.deny(privilege("t2.a", SELECT_COLUMN)); - analyze(CLIENT_SESSION, "SELECT * FROM fresh_materialized_view", accessControlManager); + analyze(CLIENT_SESSION, "SELECT * FROM fresh_materialized_view" + suffix, accessControlManager); - accessControlManager.deny(privilege("fresh_materialized_view.a", SELECT_COLUMN)); + accessControlManager.deny(privilege("fresh_materialized_view" + suffix + ".a", SELECT_COLUMN)); assertFails( CLIENT_SESSION, - "SELECT * FROM fresh_materialized_view", + "SELECT * FROM fresh_materialized_view" + suffix, accessControlManager) .hasErrorCode(PERMISSION_DENIED) - .hasMessage("Access Denied: Cannot select from columns [a, b] in table or view tpch.s1.fresh_materialized_view"); + .hasMessage("Access Denied: Cannot select from columns [a, b] in table or view tpch.s1.fresh_materialized_view" + suffix); + accessControlManager.reset(); + + // Deny access to the table referenced by the underlying query + accessControlManager.denyIdentityTable((_, table) -> !"t1".equals(table)); + if (whenStaleBehavior.orElse(WhenStaleBehavior.INLINE) == WhenStaleBehavior.FAIL) { + // In WHEN STALE FAIL mode, analysis of the underlying query is not performed, + // so access control checks on tables referenced by the query are not executed. + analyze(CLIENT_SESSION, "SELECT * FROM fresh_materialized_view" + suffix, accessControlManager); + } + else { + assertFails( + CLIENT_SESSION, + "SELECT * FROM fresh_materialized_view" + suffix, + accessControlManager) + .hasErrorCode(PERMISSION_DENIED) + .hasMessage("Access Denied: View owner does not have sufficient privileges: View owner 'some user' cannot create view that selects from tpch.s1.t1"); + } } @Test @@ -8064,7 +8098,15 @@ public void setup() ImmutableList.of(new ColumnMetadata("a", BIGINT))), FAIL)); - QualifiedObjectName freshMaterializedView = new QualifiedObjectName(TPCH_CATALOG, "s1", "fresh_materialized_view"); + createFreshMaterializedViews(metadata, testingConnectorMetadata, Optional.empty()); + createFreshMaterializedViews(metadata, testingConnectorMetadata, Optional.of(WhenStaleBehavior.FAIL)); + } + + private void createFreshMaterializedViews(Metadata metadata, TestingMetadata testingConnectorMetadata, Optional whenStaleBehavior) + { + String suffix = resolveMaterializedViewNameSuffix(whenStaleBehavior); + + QualifiedObjectName freshMaterializedView = new QualifiedObjectName(TPCH_CATALOG, "s1", "fresh_materialized_view" + suffix); inSetupTransaction(session -> metadata.createMaterializedView( session, freshMaterializedView, @@ -8074,7 +8116,7 @@ public void setup() Optional.of("s1"), ImmutableList.of(new ViewColumn("a", BIGINT.getTypeId(), Optional.empty()), new ViewColumn("b", BIGINT.getTypeId(), Optional.empty())), Optional.empty(), - Optional.empty(), + whenStaleBehavior, Optional.empty(), Identity.ofUser("some user"), ImmutableList.of(), @@ -8085,7 +8127,7 @@ public void setup() false)); testingConnectorMetadata.markMaterializedViewIsFresh(freshMaterializedView.asSchemaTableName()); - QualifiedObjectName freshMaterializedViewMismatchedColumnCount = new QualifiedObjectName(TPCH_CATALOG, "s1", "fresh_materialized_view_mismatched_column_count"); + QualifiedObjectName freshMaterializedViewMismatchedColumnCount = new QualifiedObjectName(TPCH_CATALOG, "s1", "fresh_materialized_view_mismatched_column_count" + suffix ); inSetupTransaction(session -> metadata.createMaterializedView( session, freshMaterializedViewMismatchedColumnCount, @@ -8095,7 +8137,7 @@ public void setup() Optional.of("s1"), ImmutableList.of(new ViewColumn("a", BIGINT.getTypeId(), Optional.empty())), Optional.empty(), - Optional.empty(), + whenStaleBehavior, Optional.empty(), Identity.ofUser("some user"), ImmutableList.of(), @@ -8105,7 +8147,7 @@ public void setup() false)); testingConnectorMetadata.markMaterializedViewIsFresh(freshMaterializedViewMismatchedColumnCount.asSchemaTableName()); - QualifiedObjectName freshMaterializedMismatchedColumnName = new QualifiedObjectName(TPCH_CATALOG, "s1", "fresh_materialized_view_mismatched_column_name"); + QualifiedObjectName freshMaterializedMismatchedColumnName = new QualifiedObjectName(TPCH_CATALOG, "s1", "fresh_materialized_view_mismatched_column_name" + suffix ); inSetupTransaction(session -> metadata.createMaterializedView( session, freshMaterializedMismatchedColumnName, @@ -8115,7 +8157,7 @@ public void setup() Optional.of("s1"), ImmutableList.of(new ViewColumn("a", BIGINT.getTypeId(), Optional.empty()), new ViewColumn("c", BIGINT.getTypeId(), Optional.empty())), Optional.empty(), - Optional.empty(), + whenStaleBehavior, Optional.empty(), Identity.ofUser("some user"), ImmutableList.of(), @@ -8125,7 +8167,7 @@ public void setup() false)); testingConnectorMetadata.markMaterializedViewIsFresh(freshMaterializedMismatchedColumnName.asSchemaTableName()); - QualifiedObjectName freshMaterializedMismatchedColumnType = new QualifiedObjectName(TPCH_CATALOG, "s1", "fresh_materialized_view_mismatched_column_type"); + QualifiedObjectName freshMaterializedMismatchedColumnType = new QualifiedObjectName(TPCH_CATALOG, "s1", "fresh_materialized_view_mismatched_column_type" + suffix); inSetupTransaction(session -> metadata.createMaterializedView( session, freshMaterializedMismatchedColumnType, @@ -8135,7 +8177,7 @@ public void setup() Optional.of("s1"), ImmutableList.of(new ViewColumn("a", BIGINT.getTypeId(), Optional.empty()), new ViewColumn("b", RowType.anonymousRow(TINYINT).getTypeId(), Optional.empty())), Optional.empty(), - Optional.empty(), + whenStaleBehavior, Optional.empty(), Identity.ofUser("some user"), ImmutableList.of(), @@ -8146,6 +8188,11 @@ public void setup() testingConnectorMetadata.markMaterializedViewIsFresh(freshMaterializedMismatchedColumnType.asSchemaTableName()); } + private static String resolveMaterializedViewNameSuffix(Optional whenStaleBehavior) + { + return whenStaleBehavior.map(whenStale -> "_when_stale_" + whenStale.name().toLowerCase(Locale.ENGLISH)).orElse(""); + } + @Test public void testAlterTableAddRowField() { diff --git a/core/trino-main/src/test/java/io/trino/sql/planner/TestMaterializedViews.java b/core/trino-main/src/test/java/io/trino/sql/planner/TestMaterializedViews.java index 0140c1a6cc4..27b200a3e70 100644 --- a/core/trino-main/src/test/java/io/trino/sql/planner/TestMaterializedViews.java +++ b/core/trino-main/src/test/java/io/trino/sql/planner/TestMaterializedViews.java @@ -24,9 +24,11 @@ import io.trino.metadata.TestingFunctionResolution; import io.trino.metadata.ViewColumn; import io.trino.spi.RefreshType; +import io.trino.spi.TrinoException; import io.trino.spi.connector.CatalogSchemaTableName; import io.trino.spi.connector.ColumnMetadata; import io.trino.spi.connector.Connector; +import io.trino.spi.connector.ConnectorMaterializedViewDefinition.WhenStaleBehavior; import io.trino.spi.connector.ConnectorMetadata; import io.trino.spi.connector.ConnectorSession; import io.trino.spi.connector.ConnectorTableMetadata; @@ -48,6 +50,7 @@ import io.trino.testing.TestingAccessControlManager; import io.trino.testing.TestingMetadata; import io.trino.type.DateTimes; +import org.intellij.lang.annotations.Language; import org.junit.jupiter.api.Test; import java.time.Instant; @@ -84,6 +87,7 @@ import static io.trino.testing.TestingSession.testSessionBuilder; import static io.trino.type.InternalTypeManager.TESTING_TYPE_MANAGER; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; public class TestMaterializedViews extends BasePlanTest @@ -178,103 +182,116 @@ protected PlanTester createPlanTester() return null; }); - QualifiedObjectName freshMaterializedView = new QualifiedObjectName(TEST_CATALOG_NAME, SCHEMA, "fresh_materialized_view"); - MaterializedViewDefinition materializedViewDefinition = new MaterializedViewDefinition( - "SELECT a, b FROM test_table", - Optional.of(TEST_CATALOG_NAME), - Optional.of(SCHEMA), - ImmutableList.of(new ViewColumn("a", BIGINT.getTypeId(), Optional.empty()), new ViewColumn("b", BIGINT.getTypeId(), Optional.empty())), - Optional.of(STALE_MV_STALENESS.plusHours(1)), - Optional.empty(), - Optional.empty(), - Identity.ofUser("some user"), - ImmutableList.of(), - Optional.of(new CatalogSchemaTableName(TEST_CATALOG_NAME, SCHEMA, "storage_table"))); + createFreshAndStaleMaterializedViews("fresh_materialized_view", planTester, metadata, Optional.empty()); + createFreshAndStaleMaterializedViews("fresh_materialized_view_when_stale_fail", planTester, metadata, Optional.of(WhenStaleBehavior.FAIL)); + + MaterializedViewDefinition materializedViewDefinitionWithCasts = createMaterializedViewWithCasts("materialized_view_with_casts", planTester, metadata, Optional.empty()); + createMaterializedViewWithCasts("materialized_view_with_casts_when_stale_fail", planTester, metadata, Optional.of(WhenStaleBehavior.FAIL)); + planTester.inTransaction(session -> { metadata.createMaterializedView( session, - freshMaterializedView, - materializedViewDefinition, + new QualifiedObjectName(TEST_CATALOG_NAME, SCHEMA, "stale_materialized_view_with_casts"), + materializedViewDefinitionWithCasts, ImmutableMap.of(), false, false); return null; }); - testingConnectorMetadata.markMaterializedViewIsFresh(freshMaterializedView.asSchemaTableName()); - QualifiedObjectName notFreshMaterializedView = new QualifiedObjectName(TEST_CATALOG_NAME, SCHEMA, "not_fresh_materialized_view"); + MaterializedViewDefinition materializedViewDefinitionWithTimestamp = new MaterializedViewDefinition( + "SELECT id, ts FROM timestamp_test", + Optional.of(TEST_CATALOG_NAME), + Optional.of(SCHEMA), + ImmutableList.of(new ViewColumn("id", BIGINT.getTypeId(), Optional.empty()), new ViewColumn("ts", timestampWithTimezone3.getTypeId(), Optional.empty())), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Identity.ofUser("some user"), + ImmutableList.of(), + Optional.of(new CatalogSchemaTableName(TEST_CATALOG_NAME, SCHEMA, "timestamp_test_storage"))); + QualifiedObjectName materializedViewWithTimestamp = new QualifiedObjectName(TEST_CATALOG_NAME, SCHEMA, "timestamp_mv_test"); planTester.inTransaction(session -> { metadata.createMaterializedView( session, - notFreshMaterializedView, - materializedViewDefinition, + materializedViewWithTimestamp, + materializedViewDefinitionWithTimestamp, ImmutableMap.of(), false, false); return null; }); - MaterializedViewDefinition materializedViewDefinitionWithCasts = new MaterializedViewDefinition( + testingConnectorMetadata.markMaterializedViewIsFresh(materializedViewWithTimestamp.asSchemaTableName()); + + return planTester; + } + + private void createFreshAndStaleMaterializedViews(String name, PlanTester planTester, Metadata metadata, Optional whenStaleBehavior) + { + QualifiedObjectName freshMaterializedView = new QualifiedObjectName(TEST_CATALOG_NAME, SCHEMA, name); + MaterializedViewDefinition materializedViewDefinition = new MaterializedViewDefinition( "SELECT a, b FROM test_table", Optional.of(TEST_CATALOG_NAME), Optional.of(SCHEMA), ImmutableList.of(new ViewColumn("a", BIGINT.getTypeId(), Optional.empty()), new ViewColumn("b", BIGINT.getTypeId(), Optional.empty())), - Optional.empty(), - Optional.empty(), + Optional.of(STALE_MV_STALENESS.plusHours(1)), + whenStaleBehavior, Optional.empty(), Identity.ofUser("some user"), ImmutableList.of(), - Optional.of(new CatalogSchemaTableName(TEST_CATALOG_NAME, SCHEMA, "storage_table_with_casts"))); - QualifiedObjectName materializedViewWithCasts = new QualifiedObjectName(TEST_CATALOG_NAME, SCHEMA, "materialized_view_with_casts"); + Optional.of(new CatalogSchemaTableName(TEST_CATALOG_NAME, SCHEMA, "storage_table"))); planTester.inTransaction(session -> { metadata.createMaterializedView( session, - materializedViewWithCasts, - materializedViewDefinitionWithCasts, + freshMaterializedView, + materializedViewDefinition, ImmutableMap.of(), false, false); return null; }); - testingConnectorMetadata.markMaterializedViewIsFresh(materializedViewWithCasts.asSchemaTableName()); + testingConnectorMetadata.markMaterializedViewIsFresh(freshMaterializedView.asSchemaTableName()); + QualifiedObjectName notFreshMaterializedView = new QualifiedObjectName(TEST_CATALOG_NAME, SCHEMA, "not_" + name); planTester.inTransaction(session -> { metadata.createMaterializedView( session, - new QualifiedObjectName(TEST_CATALOG_NAME, SCHEMA, "stale_materialized_view_with_casts"), - materializedViewDefinitionWithCasts, + notFreshMaterializedView, + materializedViewDefinition, ImmutableMap.of(), false, false); return null; }); + } - MaterializedViewDefinition materializedViewDefinitionWithTimestamp = new MaterializedViewDefinition( - "SELECT id, ts FROM timestamp_test", + private MaterializedViewDefinition createMaterializedViewWithCasts(String name, PlanTester planTester, Metadata metadata, Optional whenStaleBehavior) + { + MaterializedViewDefinition materializedViewDefinitionWithCasts = new MaterializedViewDefinition( + "SELECT a, b FROM test_table", Optional.of(TEST_CATALOG_NAME), Optional.of(SCHEMA), - ImmutableList.of(new ViewColumn("id", BIGINT.getTypeId(), Optional.empty()), new ViewColumn("ts", timestampWithTimezone3.getTypeId(), Optional.empty())), - Optional.empty(), + ImmutableList.of(new ViewColumn("a", BIGINT.getTypeId(), Optional.empty()), new ViewColumn("b", BIGINT.getTypeId(), Optional.empty())), Optional.empty(), + whenStaleBehavior, Optional.empty(), Identity.ofUser("some user"), ImmutableList.of(), - Optional.of(new CatalogSchemaTableName(TEST_CATALOG_NAME, SCHEMA, "timestamp_test_storage"))); - QualifiedObjectName materializedViewWithTimestamp = new QualifiedObjectName(TEST_CATALOG_NAME, SCHEMA, "timestamp_mv_test"); + Optional.of(new CatalogSchemaTableName(TEST_CATALOG_NAME, SCHEMA, "storage_table_with_casts"))); + QualifiedObjectName materializedViewWithCasts = new QualifiedObjectName(TEST_CATALOG_NAME, SCHEMA, name); planTester.inTransaction(session -> { metadata.createMaterializedView( session, - materializedViewWithTimestamp, - materializedViewDefinitionWithTimestamp, + materializedViewWithCasts, + materializedViewDefinitionWithCasts, ImmutableMap.of(), false, false); return null; }); - - testingConnectorMetadata.markMaterializedViewIsFresh(materializedViewWithTimestamp.asSchemaTableName()); - - return planTester; + testingConnectorMetadata.markMaterializedViewIsFresh(materializedViewWithCasts.asSchemaTableName()); + return materializedViewDefinitionWithCasts; } private void createMaterializedView(String materializedViewName, String query) @@ -310,6 +327,9 @@ public void testFreshMaterializedView() assertPlan("SELECT * FROM fresh_materialized_view", anyTree( tableScan("storage_table"))); + assertPlan("SELECT * FROM fresh_materialized_view_when_stale_fail", + anyTree( + tableScan("storage_table"))); } @Test @@ -325,12 +345,35 @@ public void testNotFreshMaterializedView() defaultSession, anyTree( tableScan("storage_table"))); + assertPlan( + "SELECT * FROM not_fresh_materialized_view_when_stale_fail", + defaultSession, + anyTree( + tableScan("storage_table"))); assertPlan( "SELECT * FROM not_fresh_materialized_view", futureSession, anyTree( tableScan("test_table"))); + assertThatThrownBy(() -> createPlan(futureSession, "SELECT * FROM not_fresh_materialized_view_when_stale_fail")) + .isInstanceOf(TrinoException.class) + .hasMessage("line 1:15: Materialized view 'test_catalog.tiny.not_fresh_materialized_view_when_stale_fail' is stale"); + } + + private void createPlan(Session futureSession, @Language("SQL") String sql) + { + PlanTester planTester = getPlanTester(); + planTester.inTransaction(futureSession, transactionSession -> { + planTester.createPlan( + transactionSession, + sql, + planTester.getPlanOptimizers(true), + OPTIMIZED_AND_VALIDATED, + NOOP, + createPlanOptimizersStatsCollector()); + return transactionSession.getQueryId().toString(); + }); } @Test @@ -374,14 +417,20 @@ public void testRefreshTypes() @Test public void testMaterializedViewWithCasts() + { + testMaterializedViewWithCasts("materialized_view_with_casts"); + testMaterializedViewWithCasts("materialized_view_with_casts_when_stale_fail"); + } + + private void testMaterializedViewWithCasts(String materializedViewName) { TestingAccessControlManager accessControl = getPlanTester().getAccessControl(); accessControl.columnMask( - new QualifiedObjectName(TEST_CATALOG_NAME, SCHEMA, "materialized_view_with_casts"), + new QualifiedObjectName(TEST_CATALOG_NAME, SCHEMA, materializedViewName), "a", "user", ViewExpression.builder().expression("a + 1").build()); - assertPlan("SELECT * FROM materialized_view_with_casts", + assertPlan("SELECT * FROM " + materializedViewName, anyTree( project( ImmutableMap.of( diff --git a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergConnector.java b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergConnector.java index 7b0c5ba57af..256c94727f1 100644 --- a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergConnector.java +++ b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergConnector.java @@ -50,6 +50,7 @@ import static com.google.common.collect.Sets.immutableEnumSet; import static io.trino.plugin.iceberg.IcebergErrorCode.ICEBERG_CATALOG_ERROR; import static io.trino.spi.connector.ConnectorCapabilities.MATERIALIZED_VIEW_GRACE_PERIOD; +import static io.trino.spi.connector.ConnectorCapabilities.MATERIALIZED_VIEW_WHEN_STALE_BEHAVIOR; import static io.trino.spi.connector.ConnectorCapabilities.NOT_NULL_COLUMN_CONSTRAINT; import static io.trino.spi.transaction.IsolationLevel.SERIALIZABLE; import static io.trino.spi.transaction.IsolationLevel.checkConnectorSupports; @@ -125,7 +126,8 @@ public Set getCapabilities() { return immutableEnumSet( NOT_NULL_COLUMN_CONSTRAINT, - MATERIALIZED_VIEW_GRACE_PERIOD); + MATERIALIZED_VIEW_GRACE_PERIOD, + MATERIALIZED_VIEW_WHEN_STALE_BEHAVIOR); } @Override diff --git a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergMaterializedViewDefinition.java b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergMaterializedViewDefinition.java index 9a743ecc1be..826845d7d2d 100644 --- a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergMaterializedViewDefinition.java +++ b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergMaterializedViewDefinition.java @@ -19,6 +19,7 @@ import io.airlift.json.ObjectMapperProvider; import io.trino.spi.connector.CatalogSchemaName; import io.trino.spi.connector.ConnectorMaterializedViewDefinition; +import io.trino.spi.connector.ConnectorMaterializedViewDefinition.WhenStaleBehavior; import io.trino.spi.type.TypeId; import java.time.Duration; @@ -43,6 +44,7 @@ public record IcebergMaterializedViewDefinition( Optional schema, List columns, Optional gracePeriod, + Optional whenStaleBehavior, Optional comment, List path) { @@ -79,6 +81,7 @@ public static IcebergMaterializedViewDefinition fromConnectorMaterializedViewDef .map(column -> new Column(column.getName(), column.getType(), column.getComment())) .collect(toImmutableList()), definition.getGracePeriod(), + definition.getWhenStaleBehavior(), definition.getComment(), definition.getPath()); } @@ -90,6 +93,7 @@ public static IcebergMaterializedViewDefinition fromConnectorMaterializedViewDef requireNonNull(schema, "schema is null"); columns = List.copyOf(requireNonNull(columns, "columns is null")); checkArgument(gracePeriod.isEmpty() || !gracePeriod.get().isNegative(), "gracePeriod cannot be negative: %s", gracePeriod); + requireNonNull(whenStaleBehavior, "whenStaleBehavior is null"); requireNonNull(comment, "comment is null"); path = path == null ? ImmutableList.of() : ImmutableList.copyOf(path); @@ -110,6 +114,7 @@ public String toString() schema.ifPresent(value -> joiner.add("schema=" + value)); joiner.add("columns=" + columns); gracePeriod.ifPresent(value -> joiner.add("gracePeriod≥=" + value)); + whenStaleBehavior.ifPresent(value -> joiner.add("whenStaleBehavior=" + value.name())); comment.ifPresent(value -> joiner.add("comment=" + value)); joiner.add(path.stream().map(CatalogSchemaName::toString).collect(Collectors.joining(", ", "path=(", ")"))); return getClass().getSimpleName() + joiner; diff --git a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/AbstractTrinoCatalog.java b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/AbstractTrinoCatalog.java index 6bbbd92bb2f..972fa18b0e3 100644 --- a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/AbstractTrinoCatalog.java +++ b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/AbstractTrinoCatalog.java @@ -466,7 +466,7 @@ protected ConnectorMaterializedViewDefinition getMaterializedViewDefinition( definition.schema(), toSpiMaterializedViewColumns(definition.columns()), definition.gracePeriod(), - Optional.empty(), + definition.whenStaleBehavior(), definition.comment(), owner, definition.path()); 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 0e4768f4fbd..608bdf00f2a 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 @@ -280,7 +280,6 @@ protected boolean hasBehavior(TestingConnectorBehavior connectorBehavior) SUPPORTS_LIMIT_PUSHDOWN, SUPPORTS_REFRESH_VIEW, SUPPORTS_RENAME_MATERIALIZED_VIEW_ACROSS_SCHEMAS, - SUPPORTS_CREATE_MATERIALIZED_VIEW_WHEN_STALE, SUPPORTS_TOPN_PUSHDOWN -> false; default -> super.hasBehavior(connectorBehavior); }; diff --git a/plugin/trino-lakehouse/src/main/java/io/trino/plugin/lakehouse/LakehouseConnector.java b/plugin/trino-lakehouse/src/main/java/io/trino/plugin/lakehouse/LakehouseConnector.java index 8ee552c9893..3829e2156a4 100644 --- a/plugin/trino-lakehouse/src/main/java/io/trino/plugin/lakehouse/LakehouseConnector.java +++ b/plugin/trino-lakehouse/src/main/java/io/trino/plugin/lakehouse/LakehouseConnector.java @@ -34,6 +34,7 @@ import static com.google.common.collect.Sets.immutableEnumSet; import static io.trino.spi.connector.ConnectorCapabilities.MATERIALIZED_VIEW_GRACE_PERIOD; +import static io.trino.spi.connector.ConnectorCapabilities.MATERIALIZED_VIEW_WHEN_STALE_BEHAVIOR; import static io.trino.spi.connector.ConnectorCapabilities.NOT_NULL_COLUMN_CONSTRAINT; import static io.trino.spi.transaction.IsolationLevel.READ_UNCOMMITTED; import static io.trino.spi.transaction.IsolationLevel.checkConnectorSupports; @@ -157,6 +158,6 @@ public void shutdown() @Override public Set getCapabilities() { - return immutableEnumSet(NOT_NULL_COLUMN_CONSTRAINT, MATERIALIZED_VIEW_GRACE_PERIOD); + return immutableEnumSet(NOT_NULL_COLUMN_CONSTRAINT, MATERIALIZED_VIEW_GRACE_PERIOD, MATERIALIZED_VIEW_WHEN_STALE_BEHAVIOR); } } 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 b77ec9d1a14..465790ae611 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 @@ -127,7 +127,6 @@ protected boolean hasBehavior(TestingConnectorBehavior connectorBehavior) SUPPORTS_LIMIT_PUSHDOWN, SUPPORTS_REFRESH_VIEW, SUPPORTS_RENAME_MATERIALIZED_VIEW_ACROSS_SCHEMAS, - SUPPORTS_CREATE_MATERIALIZED_VIEW_WHEN_STALE, SUPPORTS_TOPN_PUSHDOWN -> false; default -> super.hasBehavior(connectorBehavior); }; 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 4651cf1490b..867529e889f 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 @@ -1598,21 +1598,207 @@ public void testMaterializedViewGracePeriod() } @Test - public void testMaterializedViewWhenStale() + public void testMaterializedViewWhenStaleInline() + { + skipTestUnless(hasBehavior(SUPPORTS_CREATE_MATERIALIZED_VIEW)); + + if (!hasBehavior(SUPPORTS_CREATE_MATERIALIZED_VIEW_WHEN_STALE)) { + String catalog = getSession().getCatalog().orElseThrow(); + String viewName = "test_mv_when_stale_" + randomNameSuffix(); + assertQueryFails( + "CREATE MATERIALIZED VIEW " + viewName + " WHEN STALE INLINE AS SELECT * FROM nation", + "line 1:1: Catalog '%s' does not support WHEN STALE".formatted(catalog)); + return; + } + + testMaterializedViewWhenStaleInline(false); + testMaterializedViewWhenStaleInline(true); + } + + private void testMaterializedViewWhenStaleInline(boolean includeWhenStaleInlineClause) { skipTestUnless(hasBehavior(SUPPORTS_CREATE_MATERIALIZED_VIEW)); String catalog = getSession().getCatalog().orElseThrow(); - String viewName = "test_mv_when_stale_" + randomNameSuffix(); + String schema = getSession().getSchema().orElseThrow(); + + try (TestTable table = newTrinoTable("test_base_table", "AS TABLE region")) { + QualifiedObjectName baseTable = new QualifiedObjectName(catalog, schema, table.getName()); + + Session defaultSession = getSession(); + Session futureSession = Session.builder(defaultSession) + .setSystemProperty(TESTING_SESSION_TIME, Instant.now().plus(1, ChronoUnit.DAYS).toString()) + .build(); + + PlanMatchPattern readFromBaseTables = anyTree( + node(AggregationNode.class, // final + anyTree( // exchanges + node(AggregationNode.class, // partial + node(ProjectNode.class, // format() + tableScan(baseTable.objectName())))))); + PlanMatchPattern readFromStorageTable = node(OutputNode.class, node(TableScanNode.class)); + + QualifiedObjectName viewName = new QualifiedObjectName(catalog, schema, "mv_when_stale_" + randomNameSuffix()); + + if (includeWhenStaleInlineClause) { + assertUpdate( + """ + CREATE MATERIALIZED VIEW %s + GRACE PERIOD INTERVAL '1' HOUR + WHEN STALE INLINE + AS SELECT DISTINCT regionkey, format('%%s', name) name FROM %s + """.formatted(viewName, baseTable)); + assertThat(((String) computeScalar("SHOW CREATE MATERIALIZED VIEW " + viewName.objectName()))) + .contains("WHEN STALE INLINE"); + } + else { + assertUpdate( + """ + CREATE MATERIALIZED VIEW %s + GRACE PERIOD INTERVAL '1' HOUR + AS SELECT DISTINCT regionkey, format('%%s', name) name FROM %s + """.formatted(viewName, baseTable)); + assertThat(((String) computeScalar("SHOW CREATE MATERIALIZED VIEW " + viewName.objectName()))) + .doesNotContain("WHEN STALE"); + } + + String initialResults = "SELECT DISTINCT regionkey, CAST(name AS varchar) FROM region"; + + // The MV is initially not fresh + assertThat(query(defaultSession, "TABLE " + viewName)) + .hasPlan(readFromBaseTables) + .matches(initialResults); + assertThat(query(futureSession, "TABLE " + viewName)) + .hasPlan(readFromBaseTables) + .matches(initialResults); + + assertUpdate("REFRESH MATERIALIZED VIEW " + viewName, 5); + + // Right after the REFRESH, the view is FRESH (note: it could also be UNKNOWN) + boolean supportsFresh = hasBehavior(SUPPORTS_MATERIALIZED_VIEW_FRESHNESS_FROM_BASE_TABLES); + assertThat(query(defaultSession, "TABLE " + viewName)) + .hasPlan(readFromStorageTable) + .matches(initialResults); + assertThat(query(futureSession, "TABLE " + viewName)) + .hasPlan(supportsFresh ? readFromStorageTable : readFromBaseTables) + .matches(initialResults); + + // Change underlying state + assertUpdate("INSERT INTO " + baseTable + " (regionkey, name) VALUES (42, 'foo new region')", 1); + String updatedResults = initialResults + " UNION ALL VALUES (42, 'foo new region')"; + + // The materialization is stale now + assertThat(query(defaultSession, "TABLE " + viewName)) + .hasPlan(readFromStorageTable) + .matches(initialResults); + assertThat(query(futureSession, "TABLE " + viewName)) + .hasPlan(readFromBaseTables) + .matches(updatedResults); + + assertUpdate("REFRESH MATERIALIZED VIEW " + viewName, 6); + + assertThat(query(defaultSession, "TABLE " + viewName)) + .hasPlan(readFromStorageTable) + .matches(updatedResults); + assertThat(query(futureSession, "TABLE " + viewName)) + .hasPlan(supportsFresh ? readFromStorageTable : readFromBaseTables) + .matches(updatedResults); + + assertUpdate("DROP MATERIALIZED VIEW " + viewName); + } + } + + @Test + public void testMaterializedViewWhenStaleFail() + { + skipTestUnless(hasBehavior(SUPPORTS_CREATE_MATERIALIZED_VIEW)); + + String catalog = getSession().getCatalog().orElseThrow(); + String schema = getSession().getSchema().orElseThrow(); if (!hasBehavior(SUPPORTS_CREATE_MATERIALIZED_VIEW_WHEN_STALE)) { + String viewName = "test_mv_when_stale_" + randomNameSuffix(); assertQueryFails( "CREATE MATERIALIZED VIEW " + viewName + " WHEN STALE FAIL AS SELECT * FROM nation", "line 1:1: Catalog '%s' does not support WHEN STALE".formatted(catalog)); return; } - throw new UnsupportedOperationException("Not implemented"); + try (TestTable table = newTrinoTable("test_base_table", "AS TABLE region")) { + QualifiedObjectName baseTable = new QualifiedObjectName(catalog, schema, table.getName()); + + Session defaultSession = getSession(); + Session futureSession = Session.builder(defaultSession) + .setSystemProperty(TESTING_SESSION_TIME, Instant.now().plus(1, ChronoUnit.DAYS).toString()) + .build(); + + PlanMatchPattern readFromStorageTable = node(OutputNode.class, node(TableScanNode.class)); + + QualifiedObjectName viewName = new QualifiedObjectName(catalog, schema, "mv_when_stale_fail_" + randomNameSuffix()); + + assertUpdate( + """ + CREATE MATERIALIZED VIEW %s + GRACE PERIOD INTERVAL '1' HOUR + WHEN STALE FAIL + AS SELECT DISTINCT regionkey, format('%%s', name) name FROM %s + """.formatted(viewName, baseTable)); + assertThat(((String) computeScalar("SHOW CREATE MATERIALIZED VIEW " + viewName.objectName()))) + .contains("WHEN STALE FAIL"); + + String initialResults = "SELECT DISTINCT regionkey, CAST(name AS varchar) FROM region"; + + // The MV is initially not fresh + assertQueryFails(defaultSession, "TABLE " + viewName, + "line 1:1: Materialized view '%s' is stale".formatted(viewName)); + assertQueryFails(futureSession, "TABLE " + viewName, + "line 1:1: Materialized view '%s' is stale".formatted(viewName)); + + assertUpdate("REFRESH MATERIALIZED VIEW " + viewName, 5); + + // Right after the REFRESH, the view is FRESH (note: it could also be UNKNOWN) + boolean supportsFresh = hasBehavior(SUPPORTS_MATERIALIZED_VIEW_FRESHNESS_FROM_BASE_TABLES); + assertThat(query(defaultSession, "TABLE " + viewName)) + .hasPlan(readFromStorageTable) + .matches(initialResults); + if (supportsFresh) { + assertThat(query(futureSession, "TABLE " + viewName)) + .hasPlan(readFromStorageTable) + .matches(initialResults); + } + else { + assertQueryFails(futureSession, "TABLE " + viewName, + "line 1:1: Materialized view '%s' is stale".formatted(viewName)); + } + + // Change underlying state + assertUpdate("INSERT INTO " + baseTable + " (regionkey, name) VALUES (42, 'foo new region')", 1); + String updatedResults = initialResults + " UNION ALL VALUES (42, 'foo new region')"; + + // The materialization is stale now + assertThat(query(defaultSession, "TABLE " + viewName)) + .hasPlan(readFromStorageTable) + .matches(initialResults); + assertQueryFails(futureSession, "TABLE " + viewName, + "line 1:1: Materialized view '%s' is stale".formatted(viewName)); + + assertUpdate("REFRESH MATERIALIZED VIEW " + viewName, 6); + + assertThat(query(defaultSession, "TABLE " + viewName)) + .hasPlan(readFromStorageTable) + .matches(updatedResults); + if (supportsFresh) { + assertThat(query(futureSession, "TABLE " + viewName)) + .hasPlan(readFromStorageTable) + .matches(updatedResults); + } + else { + assertQueryFails(futureSession, "TABLE " + viewName, + "line 1:1: Materialized view '%s' is stale".formatted(viewName)); + } + + assertUpdate("DROP MATERIALIZED VIEW " + viewName); + } } @Test