@@ -106,6 +106,233 @@ protected QueryRunner createQueryRunner()
106106 Optional .empty ());
107107 }
108108
109+ @ Test
110+ public void testMaterializedViewPartitionFilteringThroughLogicalView ()
111+ {
112+ QueryRunner queryRunner = getQueryRunner ();
113+ String table = "orders_partitioned_lv_test" ;
114+ String materializedView = "orders_mv_lv_test" ;
115+ String logicalView = "orders_lv_test" ;
116+
117+ try {
118+ // Create a table partitioned by 'ds' (date string)
119+ queryRunner .execute (format ("CREATE TABLE %s WITH (partitioned_by = ARRAY['ds']) AS " +
120+ "SELECT orderkey, totalprice, '2025-11-10' AS ds FROM orders WHERE orderkey < 1000 " +
121+ "UNION ALL " +
122+ "SELECT orderkey, totalprice, '2025-11-11' AS ds FROM orders WHERE orderkey >= 1000 AND orderkey < 2000 " +
123+ "UNION ALL " +
124+ "SELECT orderkey, totalprice, '2025-11-12' AS ds FROM orders WHERE orderkey >= 2000 AND orderkey < 3000" , table ));
125+
126+ // Create a materialized view partitioned by 'ds'
127+ queryRunner .execute (format ("CREATE MATERIALIZED VIEW %s WITH (partitioned_by = ARRAY['ds']) AS " +
128+ "SELECT max(totalprice) as max_price, orderkey, ds FROM %s GROUP BY orderkey, ds" , materializedView , table ));
129+
130+ assertTrue (getQueryRunner ().tableExists (getSession (), materializedView ));
131+
132+ // Only refresh partition for '2025-11-10', leaving '2025-11-11' and '2025-11-12' missing
133+ assertUpdate (format ("REFRESH MATERIALIZED VIEW %s WHERE ds='2025-11-10'" , materializedView ), 255 );
134+
135+ // Create a logical view on top of the materialized view
136+ queryRunner .execute (format ("CREATE VIEW %s AS SELECT * FROM %s" , logicalView , materializedView ));
137+
138+ setReferencedMaterializedViews ((DistributedQueryRunner ) queryRunner , table , ImmutableList .of (materializedView ));
139+
140+ Session session = Session .builder (getQueryRunner ().getDefaultSession ())
141+ .setSystemProperty (CONSIDER_QUERY_FILTERS_FOR_MATERIALIZED_VIEW_PARTITIONS , "true" )
142+ .setCatalogSessionProperty (HIVE_CATALOG , MATERIALIZED_VIEW_MISSING_PARTITIONS_THRESHOLD , Integer .toString (1 ))
143+ .build ();
144+
145+ // Query the logical view with a predicate
146+ // The predicate should be pushed down to the materialized view
147+ // Since only ds='2025-11-10' is refreshed and that's what we're querying,
148+ // the materialized view should be used (not fall back to base table)
149+ String logicalViewQuery = format ("SELECT max_price, orderkey FROM %s WHERE ds='2025-11-10' ORDER BY orderkey" , logicalView );
150+ String directMvQuery = format ("SELECT max_price, orderkey FROM %s WHERE ds='2025-11-10' ORDER BY orderkey" , materializedView );
151+ String baseTableQuery = format ("SELECT max(totalprice) as max_price, orderkey FROM %s " +
152+ "WHERE ds='2025-11-10' " +
153+ "GROUP BY orderkey ORDER BY orderkey" , table );
154+
155+ MaterializedResult baseQueryResult = computeActual (session , baseTableQuery );
156+ MaterializedResult logicalViewResult = computeActual (session , logicalViewQuery );
157+ MaterializedResult directMvResult = computeActual (session , directMvQuery );
158+
159+ // All three queries should return the same results
160+ assertEquals (baseQueryResult , logicalViewResult );
161+ assertEquals (baseQueryResult , directMvResult );
162+
163+ // The plan for the logical view query should use the materialized view
164+ // (not fall back to base table) because we're only querying the refreshed partition
165+ assertPlan (session , logicalViewQuery , anyTree (
166+ constrainedTableScan (
167+ materializedView ,
168+ ImmutableMap .of ("ds" , singleValue (createVarcharType (10 ), utf8Slice ("2025-11-10" ))),
169+ ImmutableMap .of ())));
170+
171+ // Test query for a missing partition through logical view
172+ // This should fall back to base table because ds='2025-11-11' is not refreshed
173+ String logicalViewQueryMissing = format ("SELECT max_price, orderkey FROM %s WHERE ds='2025-11-11' ORDER BY orderkey" , logicalView );
174+ String baseTableQueryMissing = format ("SELECT max(totalprice) as max_price, orderkey FROM %s " +
175+ "WHERE ds='2025-11-11' " +
176+ "GROUP BY orderkey ORDER BY orderkey" , table );
177+
178+ MaterializedResult baseQueryResultMissing = computeActual (session , baseTableQueryMissing );
179+ MaterializedResult logicalViewResultMissing = computeActual (session , logicalViewQueryMissing );
180+
181+ assertEquals (baseQueryResultMissing , logicalViewResultMissing );
182+
183+ // Should fall back to base table for missing partition
184+ assertPlan (session , logicalViewQueryMissing , anyTree (
185+ constrainedTableScan (table , ImmutableMap .of (), ImmutableMap .of ())));
186+ }
187+ finally {
188+ queryRunner .execute ("DROP VIEW IF EXISTS " + logicalView );
189+ queryRunner .execute ("DROP MATERIALIZED VIEW IF EXISTS " + materializedView );
190+ queryRunner .execute ("DROP TABLE IF EXISTS " + table );
191+ }
192+ }
193+
194+ @ Test
195+ public void testMaterializedViewPartitionFilteringThroughLogicalViewWithCTE ()
196+ {
197+ QueryRunner queryRunner = getQueryRunner ();
198+ String table = "orders_partitioned_cte_test" ;
199+ String materializedView = "orders_mv_cte_test" ;
200+ String logicalView = "orders_lv_cte_test" ;
201+
202+ try {
203+ // Create a table partitioned by 'ds' (date string)
204+ queryRunner .execute (format ("CREATE TABLE %s WITH (partitioned_by = ARRAY['ds']) AS " +
205+ "SELECT orderkey, totalprice, '2025-11-10' AS ds FROM orders WHERE orderkey < 1000 " +
206+ "UNION ALL " +
207+ "SELECT orderkey, totalprice, '2025-11-11' AS ds FROM orders WHERE orderkey >= 1000 AND orderkey < 2000 " +
208+ "UNION ALL " +
209+ "SELECT orderkey, totalprice, '2025-11-12' AS ds FROM orders WHERE orderkey >= 2000 AND orderkey < 3000" , table ));
210+
211+ // Create a materialized view partitioned by 'ds'
212+ queryRunner .execute (format ("CREATE MATERIALIZED VIEW %s WITH (partitioned_by = ARRAY['ds']) AS " +
213+ "SELECT max(totalprice) as max_price, orderkey, ds FROM %s GROUP BY orderkey, ds" , materializedView , table ));
214+
215+ assertTrue (getQueryRunner ().tableExists (getSession (), materializedView ));
216+
217+ // Only refresh partition for '2025-11-11', leaving '2025-11-10' and '2025-11-12' missing
218+ assertUpdate (format ("REFRESH MATERIALIZED VIEW %s WHERE ds='2025-11-11'" , materializedView ), 248 );
219+
220+ // Create a logical view on top of the materialized view
221+ queryRunner .execute (format ("CREATE VIEW %s AS SELECT * FROM %s" , logicalView , materializedView ));
222+
223+ setReferencedMaterializedViews ((DistributedQueryRunner ) queryRunner , table , ImmutableList .of (materializedView ));
224+
225+ Session session = Session .builder (getQueryRunner ().getDefaultSession ())
226+ .setSystemProperty (CONSIDER_QUERY_FILTERS_FOR_MATERIALIZED_VIEW_PARTITIONS , "true" )
227+ .setCatalogSessionProperty (HIVE_CATALOG , MATERIALIZED_VIEW_MISSING_PARTITIONS_THRESHOLD , Integer .toString (1 ))
228+ .build ();
229+
230+ // Query the logical view through a CTE with a predicate
231+ // The predicate should be pushed down to the materialized view
232+ String cteQuery = format ("WITH PreQuery AS (SELECT * FROM %s WHERE ds='2025-11-11') " +
233+ "SELECT max_price, orderkey FROM PreQuery ORDER BY orderkey" , logicalView );
234+ String baseTableQuery = format ("SELECT max(totalprice) as max_price, orderkey FROM %s " +
235+ "WHERE ds='2025-11-11' " +
236+ "GROUP BY orderkey ORDER BY orderkey" , table );
237+
238+ MaterializedResult baseQueryResult = computeActual (session , baseTableQuery );
239+ MaterializedResult cteQueryResult = computeActual (session , cteQuery );
240+
241+ // Both queries should return the same results
242+ assertEquals (baseQueryResult , cteQueryResult );
243+
244+ // The plan for the CTE query should use the materialized view
245+ // (not fall back to base table) because we're only querying the refreshed partition
246+ assertPlan (session , cteQuery , anyTree (
247+ constrainedTableScan (
248+ materializedView ,
249+ ImmutableMap .of ("ds" , singleValue (createVarcharType (10 ), utf8Slice ("2025-11-11" ))),
250+ ImmutableMap .of ())));
251+ }
252+ finally {
253+ queryRunner .execute ("DROP VIEW IF EXISTS " + logicalView );
254+ queryRunner .execute ("DROP MATERIALIZED VIEW IF EXISTS " + materializedView );
255+ queryRunner .execute ("DROP TABLE IF EXISTS " + table );
256+ }
257+ }
258+
259+ @ Test
260+ public void testMaterializedViewPartitionFilteringInCTE ()
261+ {
262+ QueryRunner queryRunner = getQueryRunner ();
263+ String table = "orders_partitioned_mv_cte_test" ;
264+ String materializedView = "orders_mv_direct_cte_test" ;
265+
266+ try {
267+ // Create a table partitioned by 'ds' (date string)
268+ queryRunner .execute (format ("CREATE TABLE %s WITH (partitioned_by = ARRAY['ds']) AS " +
269+ "SELECT orderkey, totalprice, '2025-11-10' AS ds FROM orders WHERE orderkey < 1000 " +
270+ "UNION ALL " +
271+ "SELECT orderkey, totalprice, '2025-11-11' AS ds FROM orders WHERE orderkey >= 1000 AND orderkey < 2000 " +
272+ "UNION ALL " +
273+ "SELECT orderkey, totalprice, '2025-11-12' AS ds FROM orders WHERE orderkey >= 2000 AND orderkey < 3000" , table ));
274+
275+ // Create a materialized view partitioned by 'ds'
276+ queryRunner .execute (format ("CREATE MATERIALIZED VIEW %s WITH (partitioned_by = ARRAY['ds']) AS " +
277+ "SELECT max(totalprice) as max_price, orderkey, ds FROM %s GROUP BY orderkey, ds" , materializedView , table ));
278+
279+ assertTrue (getQueryRunner ().tableExists (getSession (), materializedView ));
280+
281+ // Only refresh partition for '2025-11-10', leaving '2025-11-11' and '2025-11-12' missing
282+ assertUpdate (format ("REFRESH MATERIALIZED VIEW %s WHERE ds='2025-11-10'" , materializedView ), 255 );
283+
284+ setReferencedMaterializedViews ((DistributedQueryRunner ) queryRunner , table , ImmutableList .of (materializedView ));
285+
286+ Session session = Session .builder (getQueryRunner ().getDefaultSession ())
287+ .setSystemProperty (CONSIDER_QUERY_FILTERS_FOR_MATERIALIZED_VIEW_PARTITIONS , "true" )
288+ .setCatalogSessionProperty (HIVE_CATALOG , MATERIALIZED_VIEW_MISSING_PARTITIONS_THRESHOLD , Integer .toString (1 ))
289+ .build ();
290+
291+ // Query the materialized view directly through a CTE with a predicate
292+ // The predicate should be used to determine which partitions are needed
293+ String cteQuery = format ("WITH PreQuery AS (SELECT * FROM %s WHERE ds='2025-11-10') " +
294+ "SELECT max_price, orderkey FROM PreQuery ORDER BY orderkey" , materializedView );
295+ String baseTableQuery = format ("SELECT max(totalprice) as max_price, orderkey FROM %s " +
296+ "WHERE ds='2025-11-10' " +
297+ "GROUP BY orderkey ORDER BY orderkey" , table );
298+
299+ MaterializedResult baseQueryResult = computeActual (session , baseTableQuery );
300+ MaterializedResult cteQueryResult = computeActual (session , cteQuery );
301+
302+ // Both queries should return the same results
303+ assertEquals (baseQueryResult , cteQueryResult );
304+
305+ // The plan for the CTE query should use the materialized view
306+ // (not fall back to base table) because we're only querying the refreshed partition
307+ assertPlan (session , cteQuery , anyTree (
308+ constrainedTableScan (
309+ materializedView ,
310+ ImmutableMap .of ("ds" , singleValue (createVarcharType (10 ), utf8Slice ("2025-11-10" ))),
311+ ImmutableMap .of ())));
312+
313+ // Test query for a missing partition through CTE
314+ // This should fall back to base table because ds='2025-11-11' is not refreshed
315+ String cteQueryMissing = format ("WITH PreQuery AS (SELECT * FROM %s WHERE ds='2025-11-11') " +
316+ "SELECT max_price, orderkey FROM PreQuery ORDER BY orderkey" , materializedView );
317+ String baseTableQueryMissing = format ("SELECT max(totalprice) as max_price, orderkey FROM %s " +
318+ "WHERE ds='2025-11-11' " +
319+ "GROUP BY orderkey ORDER BY orderkey" , table );
320+
321+ MaterializedResult baseQueryResultMissing = computeActual (session , baseTableQueryMissing );
322+ MaterializedResult cteQueryResultMissing = computeActual (session , cteQueryMissing );
323+
324+ assertEquals (baseQueryResultMissing , cteQueryResultMissing );
325+
326+ // Should fall back to base table for missing partition
327+ assertPlan (session , cteQueryMissing , anyTree (
328+ constrainedTableScan (table , ImmutableMap .of (), ImmutableMap .of ())));
329+ }
330+ finally {
331+ queryRunner .execute ("DROP MATERIALIZED VIEW IF EXISTS " + materializedView );
332+ queryRunner .execute ("DROP TABLE IF EXISTS " + table );
333+ }
334+ }
335+
109336 @ Test
110337 public void testMaterializedViewOptimization ()
111338 {
0 commit comments