diff --git a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveMaterializedViewLogicalPlanner.java b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveMaterializedViewLogicalPlanner.java index e872725458c38..8a1f9ea62a3c9 100644 --- a/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveMaterializedViewLogicalPlanner.java +++ b/presto-hive/src/test/java/com/facebook/presto/hive/TestHiveMaterializedViewLogicalPlanner.java @@ -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 " + @@ -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); } } diff --git a/presto-iceberg/src/test/java/com/facebook/presto/iceberg/TestIcebergMaterializedViewsBase.java b/presto-iceberg/src/test/java/com/facebook/presto/iceberg/TestIcebergMaterializedViewsBase.java index 34be08370f01b..62c0d76faa09c 100644 --- a/presto-iceberg/src/test/java/com/facebook/presto/iceberg/TestIcebergMaterializedViewsBase.java +++ b/presto-iceberg/src/test/java/com/facebook/presto/iceberg/TestIcebergMaterializedViewsBase.java @@ -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"); + } + } } diff --git a/presto-main-base/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java b/presto-main-base/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java index caa4acb5e6baa..09fe0e2e39b7e 100644 --- a/presto-main-base/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java +++ b/presto-main-base/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java @@ -2203,15 +2203,20 @@ protected Scope visitTable(Table table, Optional scope) } Optional 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); + } } Statement statement = analysis.getStatement(); if (optionalMaterializedView.isPresent() && statement instanceof Query) {