Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f09dcb0
[Views] Implement Materialized Views; Integrate with Spark SQL
wmoustafa Feb 28, 2024
9b3faa7
Represent the storage table using its catalog identifier
wmoustafa Mar 11, 2024
e0deb0b
Add support for replacing view version
wmoustafa Mar 13, 2024
e32a021
Update MV implementation to use new spec elements
Jul 9, 2024
1fb1e92
Add refresh-state model; update Spark MV layer
wmoustafa Mar 18, 2026
cc3bff4
Port MV support to Spark 4.1; fix DropV2ViewExec cast
wmoustafa Mar 24, 2026
e6a5abe
Fix scalestyle violations in MV Spark extensions
wmoustafa Mar 24, 2026
7033883
Fix spotless formatting in ViewBuilder.java
wmoustafa Mar 24, 2026
70021d4
Fix remaining spotless issues
wmoustafa Mar 24, 2026
5a97c32
Fix spotless and scalestyle violations across v3.5 and v4.1
wmoustafa Mar 25, 2026
aa58212
Fix spotless Scala formatting in v3.5 RewriteViewCommands and parser
wmoustafa Mar 25, 2026
60bd84c
Fix checkstyle violations in core module
wmoustafa Mar 25, 2026
4af2a89
Convert v3.5 TestMaterializedViews to JUnit 5
wmoustafa Mar 25, 2026
345e12e
Add message checks to assertThatThrownBy in MV tests
wmoustafa Mar 25, 2026
e3928de
Fix v3.5 TestMaterializedViews to skip configureValidationCatalog
wmoustafa Mar 25, 2026
3a7cb53
Propagate storage table identifier in REST CatalogHandlers.createView
wmoustafa Mar 26, 2026
ec12d45
Spark: Add REFRESH MATERIALIZED VIEW support in v3.5 and v4.1 extensions
wmoustafa Jun 23, 2026
2141f0d
Address review comments
wmoustafa Jun 23, 2026
84dfefd
Spark: Track and validate nested view state for MV freshness
wmoustafa Aug 6, 2026
9a2643b
Spark: Limit materialized view support to Spark 4.2
wmoustafa Aug 25, 2026
4df4eb5
Spark 4.2: Support materialized views
wmoustafa Sep 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions api/src/main/java/org/apache/iceberg/view/ViewBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package org.apache.iceberg.view;

import java.util.Map;
import org.apache.iceberg.catalog.TableIdentifier;
Comment thread
wmoustafa marked this conversation as resolved.
import org.apache.iceberg.catalog.ViewCatalog;

/**
Expand Down Expand Up @@ -55,6 +56,16 @@ default ViewBuilder withLocation(String location) {
throw new UnsupportedOperationException("Setting a view's location is not supported");
}

/**
* Sets the storage table identifier for a materialized view.
*
* @param storageTableIdentifier the storage table identifier
* @return this for method chaining
*/
default ViewBuilder withStorageTableIdentifier(TableIdentifier storageTableIdentifier) {
throw new UnsupportedOperationException("Setting a storage table identifier is not supported");
}

/**
* Create the view.
*
Expand Down
12 changes: 12 additions & 0 deletions api/src/main/java/org/apache/iceberg/view/ViewVersion.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.List;
import java.util.Map;
import org.apache.iceberg.catalog.Namespace;
import org.apache.iceberg.catalog.TableIdentifier;

/**
* A version of the view at a point in time.
Expand Down Expand Up @@ -78,4 +79,15 @@ default String defaultCatalog() {

/** The default namespace to use when the SQL does not contain a namespace. */
Namespace defaultNamespace();

Comment thread
wmoustafa marked this conversation as resolved.
/**
* The storage table identifier for materialized views.
*
* <p>When null, the entity is a regular view. When set, the entity is a materialized view and
* this identifies the storage table that holds the precomputed data. The storage table must be in
* the same catalog as the materialized view.
*/
default TableIdentifier storageTable() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of exposing the storage table in the metastore, this could be storageMetadataLocation
However, before refreshing the MV, the metadata location would not exist, so we'd need something to be able to distinguish whether we're dealing with a regular view or a materialized view.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed in the other comment, the spec treats the storage table as a first class object. I think we should keep the abstraction as TableIdentifier.

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -401,12 +401,12 @@ protected Map<String, String> properties() {
return catalogProperties == null ? ImmutableMap.of() : catalogProperties;
}

private class InMemoryTableOperations extends BaseMetastoreTableOperations {
protected class InMemoryTableOperations extends BaseMetastoreTableOperations {
private final FileIO fileIO;
private final TableIdentifier tableIdentifier;
private final String fullTableName;

InMemoryTableOperations(FileIO fileIO, TableIdentifier tableIdentifier) {
public InMemoryTableOperations(FileIO fileIO, TableIdentifier tableIdentifier) {
this.fileIO = fileIO;
this.tableIdentifier = tableIdentifier;
this.fullTableName = fullTableName(catalogName, tableIdentifier);
Expand Down Expand Up @@ -472,12 +472,12 @@ protected String tableName() {
}
}

private class InMemoryViewOperations extends BaseViewOperations {
protected class InMemoryViewOperations extends BaseViewOperations {
private final FileIO io;
private final TableIdentifier identifier;
private final String fullViewName;

InMemoryViewOperations(FileIO io, TableIdentifier identifier) {
public InMemoryViewOperations(FileIO io, TableIdentifier identifier) {
this.io = io;
this.identifier = identifier;
this.fullViewName = ViewUtil.fullViewName(catalogName, identifier);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,10 @@ public static LoadViewResponse createView(
.withDefaultCatalog(request.viewVersion().defaultCatalog())
.withLocation(request.location());

if (request.viewVersion().storageTable() != null) {
viewBuilder.withStorageTableIdentifier(request.viewVersion().storageTable());
}

Set<String> unsupportedRepresentations =
request.viewVersion().representations().stream()
.filter(r -> !(r instanceof SQLViewRepresentation))
Expand Down
29 changes: 23 additions & 6 deletions core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
Original file line number Diff line number Diff line change
Expand Up @@ -1718,6 +1718,7 @@ private class RESTViewBuilder implements ViewBuilder {
private String defaultCatalog = null;
private Schema schema = null;
private String location = null;
private TableIdentifier storageTableIdentifier = null;

private RESTViewBuilder(SessionContext context, TableIdentifier identifier) {
checkViewIdentifierIsValid(identifier);
Expand Down Expand Up @@ -1797,6 +1798,12 @@ public ViewBuilder withLocation(String newLocation) {
return this;
}

@Override
public ViewBuilder withStorageTableIdentifier(TableIdentifier newStorageTableIdentifier) {
this.storageTableIdentifier = newStorageTableIdentifier;
return this;
}

@Override
public View create() {
Endpoint.check(endpoints, Endpoint.V1_CREATE_VIEW);
Expand All @@ -1806,16 +1813,21 @@ public View create() {
Preconditions.checkState(
null != defaultNamespace, "Cannot create view without specifying a default namespace");

ViewVersion viewVersion =
ImmutableViewVersion.Builder versionBuilder =
ImmutableViewVersion.builder()
.versionId(1)
.schemaId(schema.schemaId())
.addAllRepresentations(representations)
.defaultNamespace(defaultNamespace)
.defaultCatalog(defaultCatalog)
.timestampMillis(System.currentTimeMillis())
.putAllSummary(EnvironmentContext.get())
.build();
.putAllSummary(EnvironmentContext.get());

if (storageTableIdentifier != null) {
versionBuilder.storageTable(storageTableIdentifier);
}

ViewVersion viewVersion = versionBuilder.build();

properties.putAll(viewOverrideProperties());

Expand Down Expand Up @@ -1906,16 +1918,21 @@ private View replace(LoadViewResponse response) {
.max(Integer::compareTo)
.orElseGet(metadata::currentVersionId);

ViewVersion viewVersion =
ImmutableViewVersion.Builder versionBuilder =
ImmutableViewVersion.builder()
.versionId(maxVersionId + 1)
.schemaId(schema.schemaId())
.addAllRepresentations(representations)
.defaultNamespace(defaultNamespace)
.defaultCatalog(defaultCatalog)
.timestampMillis(System.currentTimeMillis())
.putAllSummary(EnvironmentContext.get())
.build();
.putAllSummary(EnvironmentContext.get());

if (storageTableIdentifier != null) {
versionBuilder.storageTable(storageTableIdentifier);
}

ViewVersion viewVersion = versionBuilder.build();

properties.putAll(viewOverrideProperties());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ protected class BaseViewBuilder implements ViewBuilder {
private String defaultCatalog = null;
private Schema schema = null;
private String location = null;
private TableIdentifier storageTableIdentifier = null;

protected BaseViewBuilder(TableIdentifier identifier) {
Preconditions.checkArgument(
Expand Down Expand Up @@ -159,6 +160,12 @@ public ViewBuilder withLocation(String newLocation) {
return this;
}

@Override
public ViewBuilder withStorageTableIdentifier(TableIdentifier newStorageTableIdentifier) {
this.storageTableIdentifier = newStorageTableIdentifier;
return this;
}

@Override
public View create() {
return create(newViewOps(identifier));
Expand Down Expand Up @@ -190,16 +197,21 @@ private View create(ViewOperations ops) {
Preconditions.checkState(
null != defaultNamespace, "Cannot create view without specifying a default namespace");

ViewVersion viewVersion =
ImmutableViewVersion.Builder versionBuilder =
ImmutableViewVersion.builder()
.versionId(1)
.schemaId(schema.schemaId())
.addAllRepresentations(representations)
.defaultNamespace(defaultNamespace)
.defaultCatalog(defaultCatalog)
.timestampMillis(System.currentTimeMillis())
.putAllSummary(EnvironmentContext.get())
.build();
.putAllSummary(EnvironmentContext.get());

if (storageTableIdentifier != null) {
versionBuilder.storageTable(storageTableIdentifier);
}

ViewVersion viewVersion = versionBuilder.build();

properties.putAll(viewOverrideProperties());

Expand Down Expand Up @@ -241,16 +253,21 @@ private View replace(ViewOperations ops) {
.max(Integer::compareTo)
.orElseGet(metadata::currentVersionId);

ViewVersion viewVersion =
ImmutableViewVersion.Builder versionBuilder =
ImmutableViewVersion.builder()
.versionId(maxVersionId + 1)
.schemaId(schema.schemaId())
.addAllRepresentations(representations)
.defaultNamespace(defaultNamespace)
.defaultCatalog(defaultCatalog)
.timestampMillis(System.currentTimeMillis())
.putAllSummary(EnvironmentContext.get())
.build();
.putAllSummary(EnvironmentContext.get());

if (storageTableIdentifier != null) {
versionBuilder.storageTable(storageTableIdentifier);
}

ViewVersion viewVersion = versionBuilder.build();

properties.putAll(viewOverrideProperties());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package org.apache.iceberg.view;

import javax.annotation.Nullable;
import org.apache.iceberg.catalog.TableIdentifier;
import org.immutables.value.Value;

/**
Expand All @@ -45,4 +46,8 @@ default String operation() {
@Override
@Nullable
String defaultCatalog();

@Override
@Nullable
TableIdentifier storageTable();
}
58 changes: 58 additions & 0 deletions core/src/main/java/org/apache/iceberg/view/RefreshState.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.iceberg.view;

import java.util.List;
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;

/**
* Captures the state of source tables and views at the time of a materialized view refresh
* operation. Stored as a JSON-encoded string in the storage table's snapshot summary under the
* {@code refresh-state} key.
*/
public class RefreshState {
public static final String REFRESH_STATE_SUMMARY_KEY = "refresh-state";

/** Recorded as the snapshot id of a source table that had no snapshot when it was read. */
public static final long NO_SNAPSHOT_ID = -1L;

private final int viewVersionId;
private final List<SourceState> sourceStates;
private final long refreshStartTimestampMs;

public RefreshState(
int viewVersionId, List<SourceState> sourceStates, long refreshStartTimestampMs) {
Preconditions.checkArgument(sourceStates != null, "Source states list is required");
this.viewVersionId = viewVersionId;
this.sourceStates = sourceStates;
this.refreshStartTimestampMs = refreshStartTimestampMs;
}

public int viewVersionId() {
return viewVersionId;
}

public List<SourceState> sourceStates() {
return sourceStates;
}

public long refreshStartTimestampMs() {
return refreshStartTimestampMs;
}
}
Loading
Loading