Skip to content

Commit 6c5d9ff

Browse files
authored
fix: Make default sort order nulls last (#1766)
* fix: Make default sort order nulls last * Add entry to upgrade guide
1 parent ceb2d8d commit 6c5d9ff

5 files changed

Lines changed: 130 additions & 33 deletions

File tree

‎docs/source/user-guide/upgrade-guides.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,30 @@
1919

2020
# Upgrade Guides
2121

22+
## DataFusion 56.0.0
23+
24+
### `sort` and `order_by` now order `NULLS LAST` by default
25+
26+
Calling `.sort(...)` on a DataFrame or using the `.order_by(...)` function now
27+
orders rows with `NULLS LAST` by default, instead of `NULLS FIRST`. This follows
28+
the same behavior as SQL's `ORDER BY` or the DataFrame's `sort_by(...)`.
29+
30+
To go back to the previous behavior, we need to explicitly specify
31+
`nulls_first=True`. Example:
32+
33+
```python
34+
# sort
35+
df.sort(column("a"), nulls_first=True)
36+
37+
# order by
38+
expr = f.first_value(column("a")).over(
39+
Window(
40+
partition_by=partition,
41+
order_by=f.order_by(column("b"), nulls_first=True)
42+
)
43+
)
44+
```
45+
2246
## DataFusion 55.0.0
2347

2448
This release extends the change made in 52.0.0 to the remaining

‎python/datafusion/expr.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ def sort_or_default(e: Expr | SortExpr) -> expr_internal.SortExpr:
425425
"""Helper function to return a default Sort if an Expr is provided."""
426426
if isinstance(e, SortExpr):
427427
return e.raw_sort
428-
return SortExpr(e, ascending=True, nulls_first=True).raw_sort
428+
return SortExpr(e, ascending=True, nulls_first=False).raw_sort
429429

430430

431431
def sort_list_to_raw_sort_list(
@@ -893,7 +893,7 @@ def alias(self, name: str, metadata: dict[str, str] | None = None) -> Expr:
893893
"""
894894
return Expr(self.expr.alias(name, metadata))
895895

896-
def sort(self, ascending: bool = True, nulls_first: bool = True) -> SortExpr:
896+
def sort(self, ascending: bool = True, nulls_first: bool = False) -> SortExpr:
897897
"""Creates a sort :py:class:`Expr` from an existing :py:class:`Expr`.
898898
899899
Args:

‎python/datafusion/functions/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -892,7 +892,7 @@ def concat_ws(separator: str, *args: Expr) -> Expr:
892892
return Expr(f.concat_ws(separator, args))
893893

894894

895-
def order_by(expr: Expr, ascending: bool = True, nulls_first: bool = True) -> SortExpr:
895+
def order_by(expr: Expr, ascending: bool = True, nulls_first: bool = False) -> SortExpr:
896896
"""Creates a new sort expression.
897897
898898
Examples:

‎python/tests/test_aggregation.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ def test_bitwise_distinct_is_kept(fn):
415415
"first_value_with_null",
416416
f.first_value(
417417
column("b"),
418-
order_by=[column("b").sort(ascending=True)],
418+
order_by=[column("b").sort(ascending=True, nulls_first=True)],
419419
null_treatment=NullTreatment.RESPECT_NULLS,
420420
),
421421
[None, None],
@@ -424,7 +424,7 @@ def test_bitwise_distinct_is_kept(fn):
424424
"first_value_no_list_order_by",
425425
f.first_value(
426426
column("b"),
427-
order_by=column("b"),
427+
order_by=column("b").sort(nulls_first=True),
428428
null_treatment=NullTreatment.RESPECT_NULLS,
429429
),
430430
[None, None],

‎python/tests/test_dataframe.py‎

Lines changed: 101 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -931,29 +931,41 @@ def test_distinct():
931931
data_test_window_functions = [
932932
(
933933
"row",
934-
f.row_number(order_by=[column("b"), column("a").sort(ascending=False)]),
934+
f.row_number(
935+
order_by=[
936+
f.order_by(column("b"), nulls_first=True),
937+
column("a").sort(ascending=False),
938+
]
939+
),
935940
[4, 2, 3, 5, 7, 1, 6],
936941
),
937942
(
938943
"row_w_params",
939944
f.row_number(
940-
order_by=[column("b"), column("a")],
945+
order_by=[f.order_by(column("b"), nulls_first=True), column("a")],
941946
partition_by=[column("c")],
942947
),
943948
[2, 1, 3, 4, 2, 1, 3],
944949
),
945950
(
946951
"row_w_params_no_lists",
947952
f.row_number(
948-
order_by=column("b"),
953+
order_by=f.order_by(column("b"), nulls_first=True),
949954
partition_by=column("c"),
950955
),
951956
[2, 1, 3, 4, 2, 1, 3],
952957
),
953-
("rank", f.rank(order_by=[column("b")]), [3, 1, 3, 5, 6, 1, 6]),
958+
(
959+
"rank",
960+
f.rank(order_by=[f.order_by(column("b"), nulls_first=True)]),
961+
[3, 1, 3, 5, 6, 1, 6],
962+
),
954963
(
955964
"rank_w_params",
956-
f.rank(order_by=[column("b"), column("a")], partition_by=[column("c")]),
965+
f.rank(
966+
order_by=[f.order_by(column("b"), nulls_first=True), column("a")],
967+
partition_by=[column("c")],
968+
),
957969
[2, 1, 3, 4, 2, 1, 3],
958970
),
959971
(
@@ -963,12 +975,15 @@ def test_distinct():
963975
),
964976
(
965977
"dense_rank",
966-
f.dense_rank(order_by=[column("b")]),
978+
f.dense_rank(order_by=[f.order_by(column("b"), nulls_first=True)]),
967979
[2, 1, 2, 3, 4, 1, 4],
968980
),
969981
(
970982
"dense_rank_w_params",
971-
f.dense_rank(order_by=[column("b"), column("a")], partition_by=[column("c")]),
983+
f.dense_rank(
984+
order_by=[f.order_by(column("b"), nulls_first=True), column("a")],
985+
partition_by=[column("c")],
986+
),
972987
[2, 1, 3, 4, 2, 1, 3],
973988
),
974989
(
@@ -978,14 +993,18 @@ def test_distinct():
978993
),
979994
(
980995
"percent_rank",
981-
f.round(f.percent_rank(order_by=[column("b")]), literal(3)),
996+
f.round(
997+
f.percent_rank(order_by=[f.order_by(column("b"), nulls_first=True)]),
998+
literal(3),
999+
),
9821000
[0.333, 0.0, 0.333, 0.667, 0.833, 0.0, 0.833],
9831001
),
9841002
(
9851003
"percent_rank_w_params",
9861004
f.round(
9871005
f.percent_rank(
988-
order_by=[column("b"), column("a")], partition_by=[column("c")]
1006+
order_by=[f.order_by(column("b"), nulls_first=True), column("a")],
1007+
partition_by=[column("c")],
9891008
),
9901009
literal(3),
9911010
),
@@ -1001,14 +1020,18 @@ def test_distinct():
10011020
),
10021021
(
10031022
"cume_dist",
1004-
f.round(f.cume_dist(order_by=[column("b")]), literal(3)),
1023+
f.round(
1024+
f.cume_dist(order_by=[f.order_by(column("b"), nulls_first=True)]),
1025+
literal(3),
1026+
),
10051027
[0.571, 0.286, 0.571, 0.714, 1.0, 0.286, 1.0],
10061028
),
10071029
(
10081030
"cume_dist_w_params",
10091031
f.round(
10101032
f.cume_dist(
1011-
order_by=[column("b"), column("a")], partition_by=[column("c")]
1033+
order_by=[f.order_by(column("b"), nulls_first=True), column("a")],
1034+
partition_by=[column("c")],
10121035
),
10131036
literal(3),
10141037
),
@@ -1024,27 +1047,39 @@ def test_distinct():
10241047
),
10251048
(
10261049
"ntile",
1027-
f.ntile(2, order_by=[column("b")]),
1050+
f.ntile(2, order_by=[f.order_by(column("b"), nulls_first=True)]),
10281051
[1, 1, 1, 2, 2, 1, 2],
10291052
),
10301053
(
10311054
"ntile_w_params",
1032-
f.ntile(2, order_by=[column("b"), column("a")], partition_by=[column("c")]),
1055+
f.ntile(
1056+
2,
1057+
order_by=[f.order_by(column("b"), nulls_first=True), column("a")],
1058+
partition_by=[column("c")],
1059+
),
10331060
[1, 1, 2, 2, 1, 1, 2],
10341061
),
10351062
(
10361063
"ntile_w_params_no_lists",
1037-
f.ntile(2, order_by=column("b"), partition_by=column("c")),
1064+
f.ntile(
1065+
2,
1066+
order_by=f.order_by(column("b"), nulls_first=True),
1067+
partition_by=column("c"),
1068+
),
10381069
[1, 1, 2, 2, 1, 1, 2],
10391070
),
1040-
("lead", f.lead(column("b"), order_by=[column("b")]), [7, None, 8, 9, 9, 7, None]),
1071+
(
1072+
"lead",
1073+
f.lead(column("b"), order_by=[f.order_by(column("b"), nulls_first=True)]),
1074+
[7, None, 8, 9, 9, 7, None],
1075+
),
10411076
(
10421077
"lead_w_params",
10431078
f.lead(
10441079
column("b"),
10451080
shift_offset=2,
10461081
default_value=-1,
1047-
order_by=[column("b"), column("a")],
1082+
order_by=[f.order_by(column("b"), nulls_first=True), column("a")],
10481083
partition_by=[column("c")],
10491084
),
10501085
[8, 7, -1, -1, -1, 9, -1],
@@ -1055,19 +1090,23 @@ def test_distinct():
10551090
column("b"),
10561091
shift_offset=2,
10571092
default_value=-1,
1058-
order_by=column("b"),
1093+
order_by=f.order_by(column("b"), nulls_first=True),
10591094
partition_by=column("c"),
10601095
),
10611096
[8, 7, -1, -1, -1, 9, -1],
10621097
),
1063-
("lag", f.lag(column("b"), order_by=[column("b")]), [None, None, 7, 7, 8, None, 9]),
1098+
(
1099+
"lag",
1100+
f.lag(column("b"), order_by=[f.order_by(column("b"), nulls_first=True)]),
1101+
[None, None, 7, 7, 8, None, 9],
1102+
),
10641103
(
10651104
"lag_w_params",
10661105
f.lag(
10671106
column("b"),
10681107
shift_offset=2,
10691108
default_value=-1,
1070-
order_by=[column("b"), column("a")],
1109+
order_by=[f.order_by(column("b"), nulls_first=True), column("a")],
10711110
partition_by=[column("c")],
10721111
),
10731112
[-1, -1, None, 7, -1, -1, None],
@@ -1078,7 +1117,7 @@ def test_distinct():
10781117
column("b"),
10791118
shift_offset=2,
10801119
default_value=-1,
1081-
order_by=column("b"),
1120+
order_by=f.order_by(column("b"), nulls_first=True),
10821121
partition_by=column("c"),
10831122
),
10841123
[-1, -1, None, 7, -1, -1, None],
@@ -1106,21 +1145,30 @@ def test_distinct():
11061145
(
11071146
"first_value",
11081147
f.first_value(column("a")).over(
1109-
Window(partition_by=[column("c")], order_by=[column("b")])
1148+
Window(
1149+
partition_by=[column("c")],
1150+
order_by=[f.order_by(column("b"), nulls_first=True)],
1151+
)
11101152
),
11111153
[1, 1, 1, 1, 5, 5, 5],
11121154
),
11131155
(
11141156
"first_value_without_list_args",
11151157
f.first_value(column("a")).over(
1116-
Window(partition_by=column("c"), order_by=column("b"))
1158+
Window(
1159+
partition_by=column("c"),
1160+
order_by=f.order_by(column("b"), nulls_first=True),
1161+
)
11171162
),
11181163
[1, 1, 1, 1, 5, 5, 5],
11191164
),
11201165
(
11211166
"first_value_order_by_string",
11221167
f.first_value(column("a")).over(
1123-
Window(partition_by=[column("c")], order_by="b")
1168+
Window(
1169+
partition_by=[column("c")],
1170+
order_by=f.order_by(column("b"), nulls_first=True),
1171+
)
11241172
),
11251173
[1, 1, 1, 1, 5, 5, 5],
11261174
),
@@ -1129,15 +1177,17 @@ def test_distinct():
11291177
f.last_value(column("a")).over(
11301178
Window(
11311179
partition_by=[column("c")],
1132-
order_by=[column("b")],
1180+
order_by=[f.order_by(column("b"), nulls_first=True)],
11331181
window_frame=WindowFrame("rows", None, None),
11341182
)
11351183
),
11361184
[3, 3, 3, 3, 6, 6, 6],
11371185
),
11381186
(
11391187
"3rd_value",
1140-
f.nth_value(column("b"), 3).over(Window(order_by=[column("a")])),
1188+
f.nth_value(column("b"), 3).over(
1189+
Window(order_by=[f.order_by(column("a"), nulls_first=True)])
1190+
),
11411191
[None, None, 7, 7, 7, 7, 7],
11421192
),
11431193
(
@@ -1180,7 +1230,9 @@ def test_rank_partition_by_accepts_string(partitioned_df, partition):
11801230
def test_window_partition_by_accepts_string(partitioned_df, partition):
11811231
"""Window.partition_by accepts string identifiers."""
11821232
expr = f.first_value(column("a")).over(
1183-
Window(partition_by=partition, order_by=column("b"))
1233+
Window(
1234+
partition_by=partition, order_by=f.order_by(column("b"), nulls_first=True)
1235+
)
11841236
)
11851237
df = partitioned_df.select(expr.alias("fv"))
11861238
table = pa.Table.from_batches(df.sort(column("a")).collect())
@@ -1296,9 +1348,9 @@ def _build_array_agg_df(df):
12961348
@pytest.mark.parametrize(
12971349
("builder", "expected"),
12981350
[
1299-
pytest.param(_build_last_value_df, [3, 3, 3, 3, 6, 6, 6], id="last_value"),
1351+
pytest.param(_build_last_value_df, [1, 1, 1, 1, 5, 5, 5], id="last_value"),
13001352
pytest.param(_build_nth_value_df, [None, None, 7, 7, 7, 7, 7], id="nth_value"),
1301-
pytest.param(_build_rank_df, [1, 1, 3, 3, 5, 6, 6], id="rank"),
1353+
pytest.param(_build_rank_df, [1, 1, 3, 4, 4, 6, 6], id="rank"),
13021354
pytest.param(_build_array_agg_df, [[0, 1, 2, 3], [4, 5, 6]], id="array_agg"),
13031355
],
13041356
)
@@ -4088,3 +4140,24 @@ def test_unnest_columns_with_recursions(input_data, recursions, expected_a):
40884140
kwargs["recursions"] = recursions
40894141
result = df.unnest_columns("a", **kwargs).collect()[0]
40904142
assert result.column(0).to_pylist() == expected_a
4143+
4144+
4145+
def test_sort_default_null_behavior():
4146+
ctx = SessionContext()
4147+
ctx.sql("create table t (a int)").collect()
4148+
ctx.sql("insert into t values (3), (null), (1), (null), (4), (2)").collect()
4149+
4150+
# sort
4151+
result_sort = ctx.table("t").sort(column("a")).to_pydict()
4152+
4153+
# sort_by
4154+
result_sort_by = ctx.table("t").sort_by(column("a")).to_pydict()
4155+
4156+
# sql
4157+
result_sql = ctx.sql("select * from t order by a").to_pydict()
4158+
4159+
# order_by function
4160+
result_order_by = ctx.table("t").sort(f.order_by(column("a"))).to_pydict()
4161+
4162+
assert result_sort == result_sort_by == result_sql == result_order_by
4163+
assert result_sort == {"a": [1, 2, 3, 4, None, None]}

0 commit comments

Comments
 (0)