Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -2921,6 +2921,7 @@ public void testInsertBySelectingFromMaterializedView()
String table1 = "orders_partitioned_source";
String table2 = "orders_partitioned_target";
String table3 = "orders_from_mv";
String table4 = "orders_from_refreshed_mv";
String view = "test_orders_view";
try {
queryRunner.execute(format("CREATE TABLE %s WITH (partitioned_by = ARRAY['ds']) AS " +
Expand All @@ -2935,18 +2936,31 @@ public void testInsertBySelectingFromMaterializedView()
assertUpdate(format("CREATE TABLE %s AS SELECT * FROM %s WHERE 1=0", table2, table1), 0);
assertTrue(getQueryRunner().tableExists(getSession(), table2));

assertQueryFails(format("CREATE TABLE %s AS SELECT * FROM %s", table3, view),
".*CreateTableAsSelect by selecting from a materialized view \\w+ is not supported.*");
// CTAS from a materialized view should succeed (MV is not yet refreshed so storage is empty)
assertUpdate(format("CREATE TABLE %s AS SELECT * FROM %s WHERE ds = '2020-01-01'", table3, view), 0);
assertTrue(getQueryRunner().tableExists(getSession(), table3));

// Refresh the MV so it has data, then CTAS should read from the refreshed MV
assertUpdate(format("REFRESH MATERIALIZED VIEW %s WHERE ds = '2020-01-01'", view), 255);
assertUpdate(format("CREATE TABLE %s AS SELECT * FROM %s WHERE ds = '2020-01-01'", table4, view), 255);
assertTrue(getQueryRunner().tableExists(getSession(), table4));

assertUpdate(format("INSERT INTO %s VALUES(99999, '1-URGENT', '2019-01-02')", table2), 1);
assertUpdate(format("INSERT INTO %s SELECT * FROM %s WHERE ds = '2020-01-01'", table2, table1), 255);
assertQueryFails(format("INSERT INTO %s SELECT * FROM %s WHERE ds = '2020-01-01'", table2, view),
".*Insert by selecting from a materialized view \\w+ is not supported.*");

// INSERT from MV into a non-base-table should succeed
assertUpdate(format("INSERT INTO %s SELECT * FROM %s WHERE ds = '2020-01-01'", table2, view), 255);

// INSERT from MV into one of its base tables should fail (circular dependency)
assertQueryFails(format("INSERT INTO %s SELECT * FROM %s WHERE ds = '2020-01-01'", table1, view),
".*INSERT into table .* by selecting from materialized view .* is not supported because .* is a base table of the materialized view.*");
}
finally {
queryRunner.execute("DROP MATERIALIZED VIEW IF EXISTS " + view);
queryRunner.execute("DROP TABLE IF EXISTS " + table1);
queryRunner.execute("DROP TABLE IF EXISTS " + table2);
queryRunner.execute("DROP TABLE IF EXISTS " + table3);
queryRunner.execute("DROP TABLE IF EXISTS " + table4);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1692,4 +1692,60 @@ public void testStalenessWindowAllowsStaleReads()
assertUpdate("DROP MATERIALIZED VIEW test_staleness_window_mv");
assertUpdate("DROP TABLE test_staleness_window_base");
}

@Test
public void testInsertAndCtasFromMaterializedView()
{
assertUpdate("CREATE TABLE test_mv_insert_base (id BIGINT, name VARCHAR, value BIGINT)");
assertUpdate("INSERT INTO test_mv_insert_base VALUES (1, 'Alice', 100), (2, 'Bob', 200), (3, 'Charlie', 300)", 3);

assertUpdate("CREATE MATERIALIZED VIEW test_mv_insert_mv AS SELECT id, name, value FROM test_mv_insert_base");

try {
// CTAS from MV should succeed (no longer blanket-blocked)
assertQuerySucceeds("CREATE TABLE test_mv_insert_ctas AS SELECT * FROM test_mv_insert_mv");

// INSERT from MV into a non-base-table should succeed
assertUpdate("CREATE TABLE test_mv_insert_target (id BIGINT, name VARCHAR, value BIGINT)");
assertQuerySucceeds("INSERT INTO test_mv_insert_target SELECT * FROM test_mv_insert_mv");

// INSERT from MV into its base table should fail (circular dependency)
assertQueryFails("INSERT INTO test_mv_insert_base SELECT * FROM test_mv_insert_mv",
".*INSERT into table .* by selecting from materialized view .* is not supported because .* is a base table of the materialized view.*");
}
finally {
getQueryRunner().execute("DROP TABLE IF EXISTS test_mv_insert_ctas");
getQueryRunner().execute("DROP TABLE IF EXISTS test_mv_insert_target");
getQueryRunner().execute("DROP MATERIALIZED VIEW IF EXISTS test_mv_insert_mv");
getQueryRunner().execute("DROP TABLE IF EXISTS test_mv_insert_base");
}
}

@Test
public void testInsertFromMaterializedViewTransitiveBaseTables()
{
// Create base table -> view -> materialized view chain to test transitive base table resolution
assertUpdate("CREATE TABLE test_mv_transitive_base (id BIGINT, category VARCHAR, amount BIGINT)");
assertUpdate("INSERT INTO test_mv_transitive_base VALUES (1, 'A', 100), (2, 'B', 200), (3, 'A', 300)", 3);

assertUpdate("CREATE VIEW test_mv_transitive_view AS SELECT id, category, amount FROM test_mv_transitive_base");

assertUpdate("CREATE MATERIALIZED VIEW test_mv_transitive_mv AS SELECT id, category, amount FROM test_mv_transitive_view");

try {
// INSERT from MV into a non-base-table should succeed
assertUpdate("CREATE TABLE test_mv_transitive_target (id BIGINT, category VARCHAR, amount BIGINT)");
assertQuerySucceeds("INSERT INTO test_mv_transitive_target SELECT * FROM test_mv_transitive_mv");

// INSERT from MV into the transitive base table (underlying table of the view) should fail
assertQueryFails("INSERT INTO test_mv_transitive_base SELECT * FROM test_mv_transitive_mv",
".*INSERT into table .* by selecting from materialized view .* is not supported because .* is a base table of the materialized view.*");
}
finally {
getQueryRunner().execute("DROP TABLE IF EXISTS test_mv_transitive_target");
getQueryRunner().execute("DROP MATERIALIZED VIEW IF EXISTS test_mv_transitive_mv");
getQueryRunner().execute("DROP VIEW IF EXISTS test_mv_transitive_view");
getQueryRunner().execute("DROP TABLE IF EXISTS test_mv_transitive_base");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2203,15 +2203,20 @@ protected Scope visitTable(Table table, Optional<Scope> scope)
}

Optional<MaterializedViewDefinition> optionalMaterializedView = getMaterializedViewDefinition(session, metadataResolver, analysis.getMetadataHandle(), name);
// Prevent INSERT and CREATE TABLE when selecting from a materialized view.
if (optionalMaterializedView.isPresent()
&& (analysis.getStatement() instanceof Insert || analysis.getStatement() instanceof CreateTableAsSelect)) {
throw new SemanticException(
NOT_SUPPORTED,
table,
"%s by selecting from a materialized view %s is not supported",
analysis.getStatement().getClass().getSimpleName(),
optionalMaterializedView.get().getTable());
// Prevent INSERT when selecting from a materialized view into one of its base tables (circular dependency).
if (optionalMaterializedView.isPresent() && analysis.getStatement() instanceof Insert) {
Insert insert = (Insert) analysis.getStatement();
QualifiedObjectName targetTable = createQualifiedObjectName(session, insert, insert.getTarget(), metadata);
SchemaTableName targetSchemaTable = new SchemaTableName(targetTable.getSchemaName(), targetTable.getObjectName());
if (optionalMaterializedView.get().getBaseTables().contains(targetSchemaTable)) {
throw new SemanticException(
NOT_SUPPORTED,
table,
"INSERT into table %s by selecting from materialized view %s is not supported because %s is a base table of the materialized view",
targetTable,
optionalMaterializedView.get().getTable(),
targetTable);
}
Comment on lines +2206 to +2219
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@ceekay47 thank you for this change. The overall approach makes sense. One thing to note: currently in Iceberg connector, materialized views can be created on top of views, and views themselves can be built on other materialized views or views. Therefore, when identifying base tables that cannot be inserted into, this transitive relationship should be taken into account.

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.

@hantangwangd Thanks for the review! That's an interesting point. I believe the base tables are resolved transitively during materialized view creation () and stored in MaterializedViewDefinition. So I think this condition should handle the transitive relationship for Iceberg as well?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@ceekay47 thanks for the explanation. Yes, you are correct that the base tables of the MV have already been properly resolved transitively. Would you mind adding a test case in TestIcebergMaterializedViewBase to show this scenario?

}
Statement statement = analysis.getStatement();
if (optionalMaterializedView.isPresent() && statement instanceof Query) {
Expand Down
Loading