Skip to content

Commit da71e4a

Browse files
committed
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.
1 parent 41ee0ab commit da71e4a

File tree

12 files changed

+423
-87
lines changed

12 files changed

+423
-87
lines changed

core/trino-main/src/main/java/io/trino/execution/CreateMaterializedViewTask.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,11 @@ Analysis executeInternal(
159159
});
160160

161161
Optional<WhenStaleBehavior> whenStale = statement.getWhenStaleBehavior()
162-
.map(_ -> {
162+
.map(whenStaleBehavior -> {
163163
if (!plannerContext.getMetadata().getConnectorCapabilities(session, catalogHandle).contains(MATERIALIZED_VIEW_WHEN_STALE_BEHAVIOR)) {
164164
throw semanticException(NOT_SUPPORTED, statement, "Catalog '%s' does not support WHEN STALE", catalogName);
165165
}
166-
throw semanticException(NOT_SUPPORTED, statement, "WHEN STALE is not supported yet");
166+
return toConnectorWhenStaleBehavior(whenStaleBehavior);
167167
});
168168

169169
MaterializedViewDefinition definition = new MaterializedViewDefinition(
@@ -193,4 +193,12 @@ Analysis executeInternal(
193193
plannerContext.getMetadata().createMaterializedView(session, name, definition, properties, statement.isReplace(), statement.isNotExists());
194194
return analysis;
195195
}
196+
197+
private static WhenStaleBehavior toConnectorWhenStaleBehavior(CreateMaterializedView.WhenStaleBehavior whenStale)
198+
{
199+
return switch (whenStale) {
200+
case INLINE -> WhenStaleBehavior.INLINE;
201+
case FAIL -> WhenStaleBehavior.FAIL;
202+
};
203+
}
196204
}

core/trino-main/src/main/java/io/trino/sql/analyzer/StatementAnalyzer.java

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
import io.trino.spi.connector.ColumnHandle;
6767
import io.trino.spi.connector.ColumnMetadata;
6868
import io.trino.spi.connector.ColumnSchema;
69+
import io.trino.spi.connector.ConnectorMaterializedViewDefinition.WhenStaleBehavior;
6970
import io.trino.spi.connector.ConnectorTableMetadata;
7071
import io.trino.spi.connector.ConnectorTransactionHandle;
7172
import io.trino.spi.connector.MaterializedViewFreshness;
@@ -2285,6 +2286,7 @@ protected Scope visitTable(Table table, Optional<Scope> scope)
22852286
if (optionalMaterializedView.isPresent()) {
22862287
MaterializedViewDefinition materializedViewDefinition = optionalMaterializedView.get();
22872288
analysis.addEmptyColumnReferencesForTable(accessControl, session.getIdentity(), name);
2289+
boolean useLogicalViewSemantics = shouldUseLogicalViewSemantics(materializedViewDefinition);
22882290
if (isMaterializedViewSufficientlyFresh(session, name, materializedViewDefinition)) {
22892291
// If materialized view is sufficiently fresh with respect to its grace period, answer the query using the storage table
22902292
QualifiedName storageName = getMaterializedViewStorageTableName(materializedViewDefinition)
@@ -2293,10 +2295,13 @@ protected Scope visitTable(Table table, Optional<Scope> scope)
22932295
checkStorageTableNotRedirected(storageTableName);
22942296
TableHandle tableHandle = metadata.getTableHandle(session, storageTableName)
22952297
.orElseThrow(() -> semanticException(INVALID_VIEW, table, "Storage table '%s' does not exist", storageTableName));
2296-
return createScopeForMaterializedView(table, name, scope, materializedViewDefinition, Optional.of(tableHandle));
2298+
return createScopeForMaterializedView(table, name, scope, materializedViewDefinition, Optional.of(tableHandle), useLogicalViewSemantics);
2299+
}
2300+
else if (!useLogicalViewSemantics) {
2301+
throw semanticException(VIEW_IS_STALE, table, "Materialized view '%s' is stale", name);
22972302
}
22982303
// This is a stale materialized view and should be expanded like a logical view
2299-
return createScopeForMaterializedView(table, name, scope, materializedViewDefinition, Optional.empty());
2304+
return createScopeForMaterializedView(table, name, scope, materializedViewDefinition, Optional.empty(), useLogicalViewSemantics);
23002305
}
23012306

23022307
// This could be a reference to a logical view or a table
@@ -2385,6 +2390,15 @@ private boolean isMaterializedViewSufficientlyFresh(Session session, QualifiedOb
23852390
return staleness.compareTo(gracePeriod) <= 0;
23862391
}
23872392

2393+
private static boolean shouldUseLogicalViewSemantics(MaterializedViewDefinition materializedViewDefinition)
2394+
{
2395+
WhenStaleBehavior whenStale = materializedViewDefinition.getWhenStaleBehavior().orElse(WhenStaleBehavior.INLINE);
2396+
return switch (whenStale) {
2397+
case WhenStaleBehavior.INLINE -> true;
2398+
case WhenStaleBehavior.FAIL -> false;
2399+
};
2400+
}
2401+
23882402
private void checkStorageTableNotRedirected(QualifiedObjectName source)
23892403
{
23902404
metadata.getRedirectionAwareTableHandle(session, source).redirectedTableName().ifPresent(name -> {
@@ -2529,7 +2543,13 @@ private Scope createScopeForCommonTableExpression(Table table, Optional<Scope> s
25292543
return createAndAssignScope(table, scope, fields);
25302544
}
25312545

2532-
private Scope createScopeForMaterializedView(Table table, QualifiedObjectName name, Optional<Scope> scope, MaterializedViewDefinition view, Optional<TableHandle> storageTable)
2546+
private Scope createScopeForMaterializedView(
2547+
Table table,
2548+
QualifiedObjectName name,
2549+
Optional<Scope> scope,
2550+
MaterializedViewDefinition view,
2551+
Optional<TableHandle> storageTable,
2552+
boolean useLogicalViewSemantics)
25332553
{
25342554
return createScopeForView(
25352555
table,
@@ -2542,7 +2562,8 @@ private Scope createScopeForMaterializedView(Table table, QualifiedObjectName na
25422562
view.getPath(),
25432563
view.getColumns(),
25442564
storageTable,
2545-
true);
2565+
true,
2566+
useLogicalViewSemantics);
25462567
}
25472568

25482569
private Scope createScopeForView(Table table, QualifiedObjectName name, Optional<Scope> scope, ViewDefinition view)
@@ -2557,7 +2578,8 @@ private Scope createScopeForView(Table table, QualifiedObjectName name, Optional
25572578
view.getPath(),
25582579
view.getColumns(),
25592580
Optional.empty(),
2560-
false);
2581+
false,
2582+
true);
25612583
}
25622584

25632585
private Scope createScopeForView(
@@ -2571,7 +2593,8 @@ private Scope createScopeForView(
25712593
List<CatalogSchemaName> path,
25722594
List<ViewColumn> columns,
25732595
Optional<TableHandle> storageTable,
2574-
boolean isMaterializedView)
2596+
boolean isMaterializedView,
2597+
boolean useLogicalViewSemantics)
25752598
{
25762599
Statement statement = analysis.getStatement();
25772600
if (statement instanceof CreateView viewStatement) {
@@ -2590,18 +2613,27 @@ private Scope createScopeForView(
25902613
throw semanticException(VIEW_IS_RECURSIVE, table, "View is recursive");
25912614
}
25922615

2593-
Query query = parseView(originalSql, name, table);
2616+
if (useLogicalViewSemantics) {
2617+
Query query = parseView(originalSql, name, table);
25942618

2595-
if (!query.getFunctions().isEmpty()) {
2596-
throw semanticException(NOT_SUPPORTED, table, "View contains inline function: %s", name);
2597-
}
2619+
if (!query.getFunctions().isEmpty()) {
2620+
throw semanticException(NOT_SUPPORTED, table, "View contains inline function: %s", name);
2621+
}
25982622

2599-
analysis.registerTableForView(table, name, isMaterializedView);
2600-
RelationType descriptor = analyzeView(query, name, catalog, schema, owner, path, table);
2601-
analysis.unregisterTableForView();
2623+
analysis.registerTableForView(table, name, isMaterializedView);
2624+
RelationType descriptor = analyzeView(query, name, catalog, schema, owner, path, table);
2625+
analysis.unregisterTableForView();
26022626

2603-
checkViewStaleness(columns, descriptor.getVisibleFields(), name, table)
2604-
.ifPresent(explanation -> { throw semanticException(VIEW_IS_STALE, table, "View '%s' is stale or in invalid state: %s", name, explanation); });
2627+
checkViewStaleness(columns, descriptor.getVisibleFields(), name, table)
2628+
.ifPresent(explanation -> { throw semanticException(VIEW_IS_STALE, table, "View '%s' is stale or in invalid state: %s", name, explanation); });
2629+
2630+
if (storageTable.isEmpty()) {
2631+
analysis.registerNamedQuery(table, query);
2632+
}
2633+
}
2634+
else {
2635+
checkArgument(storageTable.isPresent(), "A storage table must be present when query analysis is skipped");
2636+
}
26052637

26062638
// Derive the type of the view from the stored definition, not from the analysis of the underlying query.
26072639
// This is needed in case the underlying table(s) changed and the query in the view now produces types that
@@ -2621,9 +2653,6 @@ private Scope createScopeForView(
26212653
List<Field> storageTableFields = analyzeStorageTable(table, viewFields, storageTable.get());
26222654
analysis.setMaterializedViewStorageTableFields(table, storageTableFields);
26232655
}
2624-
else {
2625-
analysis.registerNamedQuery(table, query);
2626-
}
26272656

26282657
Scope accessControlScope = Scope.builder()
26292658
.withRelationType(RelationId.anonymous(), new RelationType(viewFields))

core/trino-main/src/main/java/io/trino/sql/rewrite/ShowQueriesRewrite.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import io.trino.metadata.ViewPropertyManager;
4141
import io.trino.security.AccessControl;
4242
import io.trino.spi.connector.CatalogSchemaName;
43+
import io.trino.spi.connector.ConnectorMaterializedViewDefinition;
4344
import io.trino.spi.connector.ConnectorTableMetadata;
4445
import io.trino.spi.connector.SchemaTableName;
4546
import io.trino.spi.function.FunctionKind;
@@ -61,6 +62,7 @@
6162
import io.trino.sql.tree.Cast;
6263
import io.trino.sql.tree.ColumnDefinition;
6364
import io.trino.sql.tree.CreateMaterializedView;
65+
import io.trino.sql.tree.CreateMaterializedView.WhenStaleBehavior;
6466
import io.trino.sql.tree.CreateSchema;
6567
import io.trino.sql.tree.CreateTable;
6668
import io.trino.sql.tree.CreateView;
@@ -562,12 +564,21 @@ private Query showCreateMaterializedView(ShowCreate node)
562564
false,
563565
false,
564566
Optional.empty(), // TODO support GRACE PERIOD
565-
Optional.empty(), // TODO support WHEN STALE
567+
viewDefinition.flatMap(MaterializedViewDefinition::getWhenStaleBehavior)
568+
.map(Visitor::toSqlWhenStaleBehavior),
566569
propertyNodes,
567570
viewDefinition.get().getComment())).trim();
568571
return singleValueQuery("Create Materialized View", sql);
569572
}
570573

574+
private static WhenStaleBehavior toSqlWhenStaleBehavior(ConnectorMaterializedViewDefinition.WhenStaleBehavior whenStale)
575+
{
576+
return switch (whenStale) {
577+
case INLINE -> WhenStaleBehavior.INLINE;
578+
case FAIL -> WhenStaleBehavior.FAIL;
579+
};
580+
}
581+
571582
private Query showCreateView(ShowCreate node)
572583
{
573584
QualifiedObjectName objectName = createQualifiedObjectName(session, node, node.getName());

0 commit comments

Comments
 (0)