@@ -1021,6 +1021,26 @@ def test_dynamic_filter(self, duckdb_cursor):
10211021 res = duckdb_cursor .sql ("SELECT a FROM t ORDER BY a LIMIT 11" ).fetchall ()
10221022 assert len (res ) == 11
10231023
1024+ def test_dynamic_filter_nulls_first_pyarrow (self , duckdb_cursor ):
1025+ # Regression for #460(a): TOP_N with ASC NULLS FIRST pushes an
1026+ # OPTIONAL(x IS NULL OR DYNAMIC_FILTER(x)) into the arrow scan. The
1027+ # pyarrow translation must NOT collapse the OR by dropping the
1028+ # untranslatable DYNAMIC_FILTER branch — doing so produces a
1029+ # stricter (`field("x").is_null()`) predicate that drops every row.
1030+ t = pa .Table .from_pydict ({"x" : pa .array ([3 , 1 , 2 ], type = pa .int32 ())})
1031+ duckdb_cursor .register ("src" , t )
1032+ res = duckdb_cursor .sql ("SELECT * FROM src ORDER BY x ASC NULLS FIRST LIMIT 1" ).fetchall ()
1033+ assert res == [(1 ,)]
1034+
1035+ def test_dynamic_filter_nulls_first_polars_dataframe (self , duckdb_cursor ):
1036+ # pl.DataFrame is materialized to a pyarrow.Table before scanning,
1037+ # so it exercises PyarrowFilterPushdown the same way pa.Table does.
1038+ pl = pytest .importorskip ("polars" )
1039+ df = pl .DataFrame ({"x" : [3 , 1 , 2 ]})
1040+ duckdb_cursor .register ("src" , df )
1041+ res = duckdb_cursor .sql ("SELECT * FROM src ORDER BY x ASC NULLS FIRST LIMIT 1" ).fetchall ()
1042+ assert res == [(1 ,)]
1043+
10241044 def test_binary_view_filter (self , duckdb_cursor ):
10251045 """Filters on a view column work (without pushdown because pyarrow does not support view filters yet)."""
10261046 table = pa .table ({"col" : pa .array ([b"abc" , b"efg" ], type = pa .binary_view ())})
0 commit comments