Skip to content

Commit 62456c7

Browse files
committed
Respect GRACE PERIOD when querying stale mateiralized views
1 parent b472e09 commit 62456c7

File tree

17 files changed

+361
-41
lines changed

17 files changed

+361
-41
lines changed

core/trino-main/src/main/java/io/trino/FeaturesConfig.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ public class FeaturesConfig
102102
private boolean incrementalHashArrayLoadFactorEnabled = true;
103103
private boolean allowSetViewAuthorization;
104104

105+
private boolean legacyMaterializedViewGracePeriod;
105106
private boolean hideInaccessibleColumns;
106107
private boolean forceSpillingJoin;
107108

@@ -445,6 +446,21 @@ public FeaturesConfig setIncrementalHashArrayLoadFactorEnabled(boolean increment
445446
return this;
446447
}
447448

449+
@Deprecated
450+
public boolean isLegacyMaterializedViewGracePeriod()
451+
{
452+
return legacyMaterializedViewGracePeriod;
453+
}
454+
455+
@Deprecated
456+
@Config("legacy.materialized-view-grace-period")
457+
@ConfigDescription("Enable legacy handling of stale materialized views")
458+
public FeaturesConfig setLegacyMaterializedViewGracePeriod(boolean legacyMaterializedViewGracePeriod)
459+
{
460+
this.legacyMaterializedViewGracePeriod = legacyMaterializedViewGracePeriod;
461+
return this;
462+
}
463+
448464
public boolean isHideInaccessibleColumns()
449465
{
450466
return hideInaccessibleColumns;

core/trino-main/src/main/java/io/trino/SystemSessionProperties.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ public final class SystemSessionProperties
160160
public static final String RETRY_INITIAL_DELAY = "retry_initial_delay";
161161
public static final String RETRY_MAX_DELAY = "retry_max_delay";
162162
public static final String RETRY_DELAY_SCALE_FACTOR = "retry_delay_scale_factor";
163+
public static final String LEGACY_MATERIALIZED_VIEW_GRACE_PERIOD = "legacy_materialized_view_grace_period";
163164
public static final String HIDE_INACCESSIBLE_COLUMNS = "hide_inaccessible_columns";
164165
public static final String FAULT_TOLERANT_EXECUTION_TARGET_TASK_INPUT_SIZE = "fault_tolerant_execution_target_task_input_size";
165166
public static final String FAULT_TOLERANT_EXECUTION_TARGET_TASK_SPLIT_COUNT = "fault_tolerant_execution_target_task_split_count";
@@ -796,6 +797,11 @@ public SystemSessionProperties(
796797
}
797798
},
798799
false),
800+
booleanProperty(
801+
LEGACY_MATERIALIZED_VIEW_GRACE_PERIOD,
802+
"Enable legacy handling of stale materialized views",
803+
featuresConfig.isLegacyMaterializedViewGracePeriod(),
804+
false),
799805
booleanProperty(
800806
HIDE_INACCESSIBLE_COLUMNS,
801807
"When enabled non-accessible columns are silently filtered from results from SELECT * statements",
@@ -1519,6 +1525,12 @@ public static double getRetryDelayScaleFactor(Session session)
15191525
return session.getSystemProperty(RETRY_DELAY_SCALE_FACTOR, Double.class);
15201526
}
15211527

1528+
@Deprecated
1529+
public static boolean isLegacyMaterializedViewGracePeriod(Session session)
1530+
{
1531+
return session.getSystemProperty(LEGACY_MATERIALIZED_VIEW_GRACE_PERIOD, Boolean.class);
1532+
}
1533+
15221534
public static boolean isHideInaccessibleColumns(Session session)
15231535
{
15241536
return session.getSystemProperty(HIDE_INACCESSIBLE_COLUMNS, Boolean.class);

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

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
import io.trino.spi.connector.ColumnSchema;
6767
import io.trino.spi.connector.ConnectorTableMetadata;
6868
import io.trino.spi.connector.ConnectorTransactionHandle;
69-
import io.trino.spi.connector.MaterializedViewFreshness.Freshness;
69+
import io.trino.spi.connector.MaterializedViewFreshness;
7070
import io.trino.spi.connector.PointerType;
7171
import io.trino.spi.connector.SchemaTableName;
7272
import io.trino.spi.connector.TableProcedureMetadata;
@@ -251,6 +251,7 @@
251251
import io.trino.type.TypeCoercion;
252252

253253
import java.math.RoundingMode;
254+
import java.time.Duration;
254255
import java.time.Instant;
255256
import java.time.temporal.ChronoUnit;
256257
import java.util.ArrayList;
@@ -278,6 +279,7 @@
278279
import static com.google.common.collect.Iterables.getLast;
279280
import static com.google.common.collect.Iterables.getOnlyElement;
280281
import static io.trino.SystemSessionProperties.getMaxGroupingSets;
282+
import static io.trino.SystemSessionProperties.isLegacyMaterializedViewGracePeriod;
281283
import static io.trino.metadata.FunctionResolver.toPath;
282284
import static io.trino.metadata.MetadataManager.toQualifiedFunctionName;
283285
import static io.trino.metadata.MetadataUtil.createQualifiedObjectName;
@@ -2141,19 +2143,19 @@ protected Scope visitTable(Table table, Optional<Scope> scope)
21412143

21422144
Optional<MaterializedViewDefinition> optionalMaterializedView = metadata.getMaterializedView(session, name);
21432145
if (optionalMaterializedView.isPresent()) {
2144-
Freshness freshness = metadata.getMaterializedViewFreshness(session, name).getFreshness();
2145-
if (freshness == FRESH || freshness == Freshness.UNKNOWN) {
2146-
// If materialized view is current, answer the query using the storage table
2147-
QualifiedName storageName = getMaterializedViewStorageTableName(optionalMaterializedView.get())
2146+
MaterializedViewDefinition materializedViewDefinition = optionalMaterializedView.get();
2147+
if (isMaterializedViewSufficientlyFresh(session, name, materializedViewDefinition)) {
2148+
// If materialized view is sufficiently fresh with respect to its grace period, answer the query using the storage table
2149+
QualifiedName storageName = getMaterializedViewStorageTableName(materializedViewDefinition)
21482150
.orElseThrow(() -> semanticException(INVALID_VIEW, table, "Materialized view '%s' is fresh but does not have storage table name", name));
21492151
QualifiedObjectName storageTableName = createQualifiedObjectName(session, table, storageName);
21502152
checkStorageTableNotRedirected(storageTableName);
21512153
TableHandle tableHandle = metadata.getTableHandle(session, storageTableName)
21522154
.orElseThrow(() -> semanticException(INVALID_VIEW, table, "Storage table '%s' does not exist", storageTableName));
2153-
return createScopeForMaterializedView(table, name, scope, optionalMaterializedView.get(), Optional.of(tableHandle));
2155+
return createScopeForMaterializedView(table, name, scope, materializedViewDefinition, Optional.of(tableHandle));
21542156
}
21552157
// This is a stale materialized view and should be expanded like a logical view
2156-
return createScopeForMaterializedView(table, name, scope, optionalMaterializedView.get(), Optional.empty());
2158+
return createScopeForMaterializedView(table, name, scope, materializedViewDefinition, Optional.empty());
21572159
}
21582160

21592161
// This could be a reference to a logical view or a table
@@ -2213,6 +2215,43 @@ protected Scope visitTable(Table table, Optional<Scope> scope)
22132215
return tableScope;
22142216
}
22152217

2218+
private boolean isMaterializedViewSufficientlyFresh(Session session, QualifiedObjectName name, MaterializedViewDefinition materializedViewDefinition)
2219+
{
2220+
MaterializedViewFreshness materializedViewFreshness = metadata.getMaterializedViewFreshness(session, name);
2221+
MaterializedViewFreshness.Freshness freshness = materializedViewFreshness.getFreshness();
2222+
2223+
if (isLegacyMaterializedViewGracePeriod(session)) {
2224+
return switch (freshness) {
2225+
case FRESH, UNKNOWN -> true;
2226+
case STALE -> false;
2227+
};
2228+
}
2229+
2230+
if (freshness == FRESH) {
2231+
return true;
2232+
}
2233+
Optional<Instant> lastFreshTime = materializedViewFreshness.getLastFreshTime();
2234+
if (lastFreshTime.isEmpty()) {
2235+
// E.g. never refreshed, or connector not updated to report fresh time
2236+
return false;
2237+
}
2238+
if (materializedViewDefinition.getGracePeriod().isEmpty()) {
2239+
// Unlimited grace period
2240+
return true;
2241+
}
2242+
Duration gracePeriod = materializedViewDefinition.getGracePeriod().get();
2243+
if (gracePeriod.isZero()) {
2244+
// Consider 0 as a special value meaning "do not accept any staleness". This makes 0 more reliable, and more likely what user wanted,
2245+
// regardless of lastFreshTime, query time or rounding.
2246+
return false;
2247+
}
2248+
2249+
// Can be negative
2250+
// TODO should we compare lastFreshTime with session.start() or with current time? The freshness is calculated with respect to current state of things.
2251+
Duration staleness = Duration.between(lastFreshTime.get(), sessionTimeProvider.getStart(session));
2252+
return staleness.compareTo(gracePeriod) <= 0;
2253+
}
2254+
22162255
private void checkStorageTableNotRedirected(QualifiedObjectName source)
22172256
{
22182257
metadata.getRedirectionAwareTableHandle(session, source).getRedirectedTableName().ifPresent(name -> {

core/trino-main/src/main/java/io/trino/testing/TestingMetadata.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
import io.trino.spi.statistics.TableStatisticsMetadata;
4646
import io.trino.spi.type.Type;
4747

48+
import java.time.Duration;
49+
import java.time.Instant;
4850
import java.util.ArrayList;
4951
import java.util.Collection;
5052
import java.util.HashSet;
@@ -68,6 +70,8 @@
6870
public class TestingMetadata
6971
implements ConnectorMetadata
7072
{
73+
public static final Duration STALE_MV_STALENESS = Duration.ofHours(7);
74+
7175
private final ConcurrentMap<SchemaTableName, ConnectorTableMetadata> tables = new ConcurrentHashMap<>();
7276
private final ConcurrentMap<SchemaTableName, ConnectorViewDefinition> views = new ConcurrentHashMap<>();
7377
private final ConcurrentMap<SchemaTableName, ConnectorMaterializedViewDefinition> materializedViews = new ConcurrentHashMap<>();
@@ -271,7 +275,10 @@ public void dropMaterializedView(ConnectorSession session, SchemaTableName viewN
271275
@Override
272276
public MaterializedViewFreshness getMaterializedViewFreshness(ConnectorSession session, SchemaTableName name)
273277
{
274-
return new MaterializedViewFreshness(freshMaterializedViews.contains(name) ? FRESH : STALE);
278+
boolean fresh = freshMaterializedViews.contains(name);
279+
return new MaterializedViewFreshness(
280+
fresh ? FRESH : STALE,
281+
fresh ? Optional.empty() : Optional.of(Instant.now().minus(STALE_MV_STALENESS)));
275282
}
276283

277284
public void markMaterializedViewIsFresh(SchemaTableName name)

core/trino-main/src/test/java/io/trino/sql/analyzer/TestAnalyzer.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
import org.testng.annotations.BeforeClass;
101101
import org.testng.annotations.Test;
102102

103+
import java.time.Duration;
103104
import java.util.List;
104105
import java.util.Optional;
105106
import java.util.function.Consumer;
@@ -6749,7 +6750,7 @@ public void setup()
67496750
Optional.of(TPCH_CATALOG),
67506751
Optional.of("s1"),
67516752
ImmutableList.of(new ViewColumn("a", BIGINT.getTypeId(), Optional.empty())),
6752-
Optional.empty(),
6753+
Optional.of(Duration.ZERO),
67536754
Optional.of("comment"),
67546755
Identity.ofUser("user"),
67556756
Optional.empty(),
@@ -6878,7 +6879,7 @@ public void setup()
68786879
Optional.of(TPCH_CATALOG),
68796880
Optional.of("s1"),
68806881
ImmutableList.of(new ViewColumn("a", BIGINT.getTypeId(), Optional.empty())),
6881-
Optional.empty(),
6882+
Optional.of(Duration.ZERO),
68826883
Optional.empty(),
68836884
Identity.ofUser("some user"),
68846885
Optional.of(new CatalogSchemaTableName(TPCH_CATALOG, "s1", "t1")),

core/trino-main/src/test/java/io/trino/sql/analyzer/TestFeaturesConfig.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public void testDefaults()
6262
.setOmitDateTimeTypePrecision(false)
6363
.setLegacyCatalogRoles(false)
6464
.setIncrementalHashArrayLoadFactorEnabled(true)
65+
.setLegacyMaterializedViewGracePeriod(false)
6566
.setHideInaccessibleColumns(false)
6667
.setAllowSetViewAuthorization(false)
6768
.setForceSpillingJoin(false)
@@ -97,6 +98,7 @@ public void testExplicitPropertyMappings()
9798
.put("deprecated.omit-datetime-type-precision", "true")
9899
.put("deprecated.legacy-catalog-roles", "true")
99100
.put("incremental-hash-array-load-factor.enabled", "false")
101+
.put("legacy.materialized-view-grace-period", "true")
100102
.put("hide-inaccessible-columns", "true")
101103
.put("legacy.allow-set-view-authorization", "true")
102104
.put("force-spilling-join-operator", "true")
@@ -129,6 +131,7 @@ public void testExplicitPropertyMappings()
129131
.setOmitDateTimeTypePrecision(true)
130132
.setLegacyCatalogRoles(true)
131133
.setIncrementalHashArrayLoadFactorEnabled(false)
134+
.setLegacyMaterializedViewGracePeriod(true)
132135
.setHideInaccessibleColumns(true)
133136
.setAllowSetViewAuthorization(true)
134137
.setForceSpillingJoin(true)

core/trino-main/src/test/java/io/trino/sql/planner/TestMaterializedViews.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import com.google.common.collect.ImmutableList;
1717
import com.google.common.collect.ImmutableMap;
1818
import io.trino.Session;
19+
import io.trino.SystemSessionProperties;
1920
import io.trino.connector.StaticConnectorFactory;
2021
import io.trino.metadata.MaterializedViewDefinition;
2122
import io.trino.metadata.Metadata;
@@ -39,6 +40,8 @@
3940
import io.trino.testing.TestingMetadata;
4041
import org.testng.annotations.Test;
4142

43+
import java.time.Instant;
44+
import java.time.temporal.ChronoUnit;
4245
import java.util.List;
4346
import java.util.Map;
4447
import java.util.Optional;
@@ -56,6 +59,7 @@
5659
import static io.trino.sql.planner.assertions.PlanMatchPattern.values;
5760
import static io.trino.sql.planner.plan.ExchangeNode.Scope.LOCAL;
5861
import static io.trino.testing.TestingHandles.TEST_CATALOG_NAME;
62+
import static io.trino.testing.TestingMetadata.STALE_MV_STALENESS;
5963
import static io.trino.testing.TestingSession.testSessionBuilder;
6064

6165
public class TestMaterializedViews
@@ -125,7 +129,7 @@ protected LocalQueryRunner createLocalQueryRunner()
125129
Optional.of(TEST_CATALOG_NAME),
126130
Optional.of(SCHEMA),
127131
ImmutableList.of(new ViewColumn("a", BIGINT.getTypeId(), Optional.empty()), new ViewColumn("b", BIGINT.getTypeId(), Optional.empty())),
128-
Optional.empty(),
132+
Optional.of(STALE_MV_STALENESS.plusHours(1)),
129133
Optional.empty(),
130134
Identity.ofUser("some user"),
131135
Optional.of(new CatalogSchemaTableName(TEST_CATALOG_NAME, SCHEMA, "storage_table")),
@@ -198,7 +202,28 @@ public void testFreshMaterializedView()
198202
@Test
199203
public void testNotFreshMaterializedView()
200204
{
201-
assertPlan("SELECT * FROM not_fresh_materialized_view",
205+
Session defaultSession = getQueryRunner().getDefaultSession();
206+
Session legacyGracePeriod = Session.builder(defaultSession)
207+
.setSystemProperty(SystemSessionProperties.LEGACY_MATERIALIZED_VIEW_GRACE_PERIOD, "true")
208+
.build();
209+
Session futureSession = Session.builder(defaultSession)
210+
.setStart(Instant.now().plus(1, ChronoUnit.DAYS))
211+
.build();
212+
213+
assertPlan(
214+
"SELECT * FROM not_fresh_materialized_view",
215+
defaultSession,
216+
anyTree(
217+
tableScan("storage_table")));
218+
219+
assertPlan(
220+
"SELECT * FROM not_fresh_materialized_view",
221+
legacyGracePeriod,
222+
anyTree(
223+
tableScan("test_table")));
224+
assertPlan(
225+
"SELECT * FROM not_fresh_materialized_view",
226+
futureSession,
202227
anyTree(
203228
tableScan("test_table")));
204229
}

core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorMaterializedViewDefinition.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public ConnectorMaterializedViewDefinition(
5454
catalog,
5555
schema,
5656
columns,
57-
Optional.empty(),
57+
Optional.of(Duration.ZERO),
5858
comment,
5959
owner,
6060
properties);

docs/src/main/sphinx/sql/create-materialized-view.rst

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Synopsis
99
1010
CREATE [ OR REPLACE ] MATERIALIZED VIEW
1111
[ IF NOT EXISTS ] view_name
12+
[ GRACE PERIOD interval ]
1213
[ COMMENT string ]
1314
[ WITH properties ]
1415
AS query
@@ -29,16 +30,19 @@ materialized views, as compared to each time of accessing the view. Multiple
2930
reads of view data over time, or by multiple users, all trigger repeated
3031
processing. This is avoided for materialized views.
3132

32-
When the underlying data changes, the materialized view becomes out of sync with
33-
the source tables. Update the data in the materialized view with the
34-
:doc:`refresh-materialized-view` statement.
35-
3633
The optional ``OR REPLACE`` clause causes the materialized view to be replaced
3734
if it already exists rather than raising an error.
3835

3936
The optional ``IF NOT EXISTS`` clause causes the materialized view only to be
4037
created or replaced if it does not exist yet.
4138

39+
The optional ``GRACE PERIOD`` clause specifies how long the query materialization
40+
is used for querying. If the time elapsed since last materialized view refresh
41+
is greater than the grace period, the materialized view acts as a normal view and
42+
the materialized data is not used. If not specified, the grace period defaults to
43+
infinity. See :doc:`refresh-materialized-view` for more about refreshing
44+
materialized views.
45+
4246
The optional ``COMMENT`` clause causes a ``string`` comment to be stored with
4347
the metadata about the materialized view. The comment is displayed with the
4448
:doc:`show-create-materialized-view` statement and is available in the table

plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergConnector.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040

4141
import static com.google.common.collect.ImmutableList.toImmutableList;
4242
import static com.google.common.collect.Sets.immutableEnumSet;
43+
import static io.trino.spi.connector.ConnectorCapabilities.MATERIALIZED_VIEW_GRACE_PERIOD;
4344
import static io.trino.spi.connector.ConnectorCapabilities.NOT_NULL_COLUMN_CONSTRAINT;
4445
import static io.trino.spi.transaction.IsolationLevel.SERIALIZABLE;
4546
import static io.trino.spi.transaction.IsolationLevel.checkConnectorSupports;
@@ -100,7 +101,9 @@ public IcebergConnector(
100101
@Override
101102
public Set<ConnectorCapabilities> getCapabilities()
102103
{
103-
return immutableEnumSet(NOT_NULL_COLUMN_CONSTRAINT);
104+
return immutableEnumSet(
105+
NOT_NULL_COLUMN_CONSTRAINT,
106+
MATERIALIZED_VIEW_GRACE_PERIOD);
104107
}
105108

106109
@Override

0 commit comments

Comments
 (0)