Skip to content

Commit 9ca08e3

Browse files
committed
core: implement rank() window function
rank() assigns the same value to all rows in a peer group (rows with equal ORDER BY values) and skips ranks by group size, so [a,a,b,c,c] ordered ascending becomes [1,1,3,4,4]. Wire 'rank' name resolution to WindowFunc::Rank and add step/value arms in op_window_step / op_window_value. The state machine mirrors SQLite's rankStepFunc / rankValueFunc: payload[0] is the current rank value (cleared by AggValue, latched by the next step), payload[1] is the rows-seen counter (always increments). The 'latch on zero' trick lets every peer in a group read the same rank without an explicit peer-detection signal — AggValue's clear is what tells the next step 'you start a new peer group'. Verification: - New tests/window/rank.sqltest covers peers/no-peers, partition, no-ORDER-BY, NULL handling, empty input, multi-column ORDER BY, DESC, COLLATE NOCASE peer detection, partition reset with repeated values, alongside row_number, and two rank windows with different orderings. - Updates two cases in tests/window/memory.sqltest that previously expected 'no such function: rank' — one becomes a passing test, the other now waits on dense_rank().
1 parent 7f3d8bb commit 9ca08e3

5 files changed

Lines changed: 261 additions & 7 deletions

File tree

core/function.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ impl WindowFunc {
518518
/// drifts ahead of the resolver and users get "no such function" when
519519
/// they try to call them.
520520
pub fn is_implemented(&self) -> bool {
521-
matches!(self, Self::RowNumber)
521+
matches!(self, Self::RowNumber | Self::Rank)
522522
}
523523

524524
/// The hardcoded frame this built-in evaluates over, overriding any
@@ -1637,6 +1637,12 @@ impl Func {
16371637
}
16381638
Ok(Some(Self::Window(WindowFunc::RowNumber)))
16391639
}
1640+
"rank" => {
1641+
if arg_count != 0 {
1642+
crate::bail_parse_error!("wrong number of arguments to function {}()", name)
1643+
}
1644+
Ok(Some(Self::Window(WindowFunc::Rank)))
1645+
}
16401646
"timediff" => {
16411647
if arg_count != 2 {
16421648
crate::bail_parse_error!("wrong number of arguments to function {}()", name)

core/vdbe/execute.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6418,6 +6418,43 @@ fn op_window_step(
64186418
};
64196419
*counter += 1;
64206420
}
6421+
// rank() — mirrors SQLite's CallCount-based rankStepFunc.
6422+
//
6423+
// State (payload):
6424+
// [0] = current rank value (what AggValue returns; cleared to 0 by
6425+
// AggValue so the next step can detect that AggValue just ran).
6426+
// [1] = rows-seen counter for the partition (always increments).
6427+
//
6428+
// The "latch on zero" trick: AggValue clears payload[0] every time it
6429+
// runs, and a flush only happens at peer-group / partition boundaries.
6430+
// So when this step observes payload[0] == 0, it knows AggValue just
6431+
// fired on the prior peer group — meaning this row begins a fresh peer
6432+
// group, and the current row position (rows_seen) is the new rank.
6433+
// Subsequent rows in the same peer group see payload[0] != 0 and leave
6434+
// the rank value untouched, so every peer reads the same rank.
6435+
WindowFunc::Rank => {
6436+
if let Register::Value(Value::Null) = state.registers[acc_reg] {
6437+
state.registers[acc_reg] = Register::Aggregate(AggContext::Builtin(vec![
6438+
Value::from_i64(0),
6439+
Value::from_i64(0),
6440+
]));
6441+
}
6442+
let Register::Aggregate(AggContext::Builtin(payload)) = &mut state.registers[acc_reg]
6443+
else {
6444+
unreachable!("rank accumulator must be a Builtin payload");
6445+
};
6446+
let Value::Numeric(Numeric::Integer(rows_seen)) = &mut payload[1] else {
6447+
unreachable!("rank rows_seen counter must be Integer");
6448+
};
6449+
*rows_seen += 1;
6450+
let rows_seen = *rows_seen;
6451+
let Value::Numeric(Numeric::Integer(rank)) = &mut payload[0] else {
6452+
unreachable!("rank current value must be Integer");
6453+
};
6454+
if *rank == 0 {
6455+
*rank = rows_seen;
6456+
}
6457+
}
64216458
other => {
64226459
return Err(LimboError::InternalError(format!(
64236460
"window function {other} reached runtime dispatch but has no handler"
@@ -6444,6 +6481,18 @@ fn op_window_value(
64446481
)))
64456482
}
64466483
},
6484+
WindowFunc::Rank => {
6485+
// Read current rank, then clear it so the next peer group's first
6486+
// AggStep latches a fresh value (matches SQLite's rankValueFunc).
6487+
let Register::Aggregate(AggContext::Builtin(payload)) = &mut state.registers[acc_reg]
6488+
else {
6489+
return Err(LimboError::InternalError(format!(
6490+
"rank accumulator in unexpected register state: {:?}",
6491+
state.registers[acc_reg]
6492+
)));
6493+
};
6494+
std::mem::replace(&mut payload[0], Value::from_i64(0))
6495+
}
64476496
other => {
64486497
return Err(LimboError::InternalError(format!(
64496498
"window function {other} reached runtime dispatch but has no handler"

testing/sqltests/tests/pragma/default.sqltest

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,11 +284,18 @@ expect {
284284
row_number|w|0
285285
}
286286

287+
test pragma-function-list-window-rank {
288+
SELECT name, type, narg FROM pragma_function_list WHERE name = 'rank';
289+
}
290+
expect {
291+
rank|w|0
292+
}
293+
287294
@skip-if sqlite "sqlite implements these window functions; we don't yet, so they must not be advertised in pragma_function_list"
288295
test pragma-function-list-omits-unimplemented-window-functions {
289296
SELECT COUNT(*) FROM pragma_function_list
290297
WHERE name IN (
291-
'rank','dense_rank','percent_rank','cume_dist','ntile',
298+
'dense_rank','percent_rank','cume_dist','ntile',
292299
'lag','lead','first_value','last_value','nth_value'
293300
);
294301
}

testing/sqltests/tests/window/memory.sqltest

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -448,15 +448,15 @@ setup named_window_rank_data {
448448
}
449449

450450
@setup named_window_rank_data
451-
@skip-if sqlite "TODO: rank() and dense_rank() are not yet implemented."
451+
@skip-if sqlite "TODO: dense_rank() is not yet implemented."
452452
test named-window-rank-dense-rank {
453453
SELECT a, rank() OVER w1, dense_rank() OVER w1
454454
FROM t_nw_rank
455455
WINDOW w1 AS (ORDER BY b)
456456
ORDER BY a;
457457
}
458458
expect error {
459-
no such function: rank
459+
no such function: dense_rank
460460
}
461461

462462
setup named_window_ntile_data {
@@ -571,16 +571,17 @@ setup named_window_group_data {
571571
}
572572

573573
@setup named_window_group_data
574-
@skip-if sqlite "TODO: rank() is not yet implemented."
575574
test named-window-with-group-by {
576575
SELECT a, sum(b) AS s, rank() OVER w1
577576
FROM t_nw_grp
578577
GROUP BY a
579578
WINDOW w1 AS (ORDER BY sum(b))
580579
ORDER BY a;
581580
}
582-
expect error {
583-
no such function: rank
581+
expect {
582+
x|3|1
583+
y|7|3
584+
z|5|2
584585
}
585586

586587
@setup named_window_data
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
@database :memory:
2+
3+
setup rank_base {
4+
CREATE TABLE rank_items(id INTEGER PRIMARY KEY, g TEXT, v INTEGER);
5+
INSERT INTO rank_items VALUES
6+
(1, 'a', 10),
7+
(2, 'a', 10),
8+
(3, 'a', 20),
9+
(4, 'b', 5),
10+
(5, 'b', 5),
11+
(6, 'b', 5),
12+
(7, 'c', 100);
13+
}
14+
15+
@setup rank_base
16+
test rank-basic-with-peers {
17+
SELECT id, v, rank() OVER (ORDER BY v) FROM rank_items ORDER BY id;
18+
}
19+
expect {
20+
1|10|4
21+
2|10|4
22+
3|20|6
23+
4|5|1
24+
5|5|1
25+
6|5|1
26+
7|100|7
27+
}
28+
29+
@setup rank_base
30+
test rank-partition-by-group {
31+
SELECT id, g, v, rank() OVER (PARTITION BY g ORDER BY v) FROM rank_items ORDER BY id;
32+
}
33+
expect {
34+
1|a|10|1
35+
2|a|10|1
36+
3|a|20|3
37+
4|b|5|1
38+
5|b|5|1
39+
6|b|5|1
40+
7|c|100|1
41+
}
42+
43+
@setup rank_base
44+
test rank-no-order-by {
45+
SELECT id, rank() OVER (PARTITION BY g) FROM rank_items ORDER BY id;
46+
}
47+
expect {
48+
1|1
49+
2|1
50+
3|1
51+
4|1
52+
5|1
53+
6|1
54+
7|1
55+
}
56+
57+
setup rank_nulls {
58+
CREATE TABLE rank_n(v INTEGER);
59+
INSERT INTO rank_n VALUES (NULL), (NULL), (1), (2), (2);
60+
}
61+
62+
@setup rank_nulls
63+
test rank-null-values-share-rank {
64+
SELECT v, rank() OVER (ORDER BY v) FROM rank_n ORDER BY v;
65+
}
66+
expect {
67+
|1
68+
|1
69+
1|3
70+
2|4
71+
2|4
72+
}
73+
74+
setup rank_empty {
75+
CREATE TABLE rank_e(v INTEGER);
76+
}
77+
78+
@setup rank_empty
79+
test rank-empty-input {
80+
SELECT v, rank() OVER (ORDER BY v) FROM rank_e;
81+
}
82+
expect {
83+
}
84+
85+
@setup rank_base
86+
test rank-alongside-row-number {
87+
SELECT id, v,
88+
rank() OVER (ORDER BY v),
89+
row_number() OVER (ORDER BY v)
90+
FROM rank_items ORDER BY id;
91+
}
92+
expect {
93+
1|10|4|4
94+
2|10|4|5
95+
3|20|6|6
96+
4|5|1|1
97+
5|5|1|2
98+
6|5|1|3
99+
7|100|7|7
100+
}
101+
102+
setup rank_multikey {
103+
CREATE TABLE rank_mk(id INTEGER PRIMARY KEY, a INTEGER, b INTEGER);
104+
INSERT INTO rank_mk VALUES
105+
(1, 1, 1),
106+
(2, 1, 1),
107+
(3, 1, 2),
108+
(4, 2, 1);
109+
}
110+
111+
# Peers are defined by the full ORDER BY tuple, not by any one column.
112+
@setup rank_multikey
113+
test rank-multi-column-order-by {
114+
SELECT id, a, b, rank() OVER (ORDER BY a, b) FROM rank_mk ORDER BY id;
115+
}
116+
expect {
117+
1|1|1|1
118+
2|1|1|1
119+
3|1|2|3
120+
4|2|1|4
121+
}
122+
123+
@setup rank_multikey
124+
test rank-order-by-desc {
125+
SELECT id, a, rank() OVER (ORDER BY a DESC) FROM rank_mk ORDER BY id;
126+
}
127+
expect {
128+
1|1|2
129+
2|1|2
130+
3|1|2
131+
4|2|1
132+
}
133+
134+
setup rank_collate {
135+
CREATE TABLE rank_c(v TEXT COLLATE NOCASE);
136+
INSERT INTO rank_c VALUES ('Apple'), ('apple'), ('Banana'), ('banana');
137+
}
138+
139+
# Peer detection respects the column's collation: 'Apple' and 'apple'
140+
# compare equal under NOCASE, so they share rank and the next group
141+
# starts at 3 (rank gap reflects the two-row peer group).
142+
@setup rank_collate
143+
test rank-collate-nocase-peers {
144+
SELECT v, rank() OVER (ORDER BY v) FROM rank_c ORDER BY v;
145+
}
146+
expect {
147+
Apple|1
148+
apple|1
149+
Banana|3
150+
banana|3
151+
}
152+
153+
setup rank_partition_repeated {
154+
CREATE TABLE rank_pr(g TEXT, v INTEGER);
155+
INSERT INTO rank_pr VALUES
156+
('a', 1), ('a', 1), ('a', 2),
157+
('b', 1), ('b', 2), ('b', 2), ('b', 3);
158+
}
159+
160+
# Values reappear across partitions; rank must reset to 1 at each
161+
# partition boundary regardless of what came before.
162+
@setup rank_partition_repeated
163+
test rank-resets-across-partitions-with-repeated-values {
164+
SELECT g, v, rank() OVER (PARTITION BY g ORDER BY v) FROM rank_pr ORDER BY g, v;
165+
}
166+
expect {
167+
a|1|1
168+
a|1|1
169+
a|2|3
170+
b|1|1
171+
b|2|2
172+
b|2|2
173+
b|3|4
174+
}
175+
176+
@setup rank_base
177+
test two-rank-windows-different-orderings {
178+
SELECT id, v,
179+
rank() OVER (ORDER BY v),
180+
rank() OVER (ORDER BY v DESC)
181+
FROM rank_items ORDER BY id;
182+
}
183+
expect {
184+
1|10|4|3
185+
2|10|4|3
186+
3|20|6|2
187+
4|5|1|5
188+
5|5|1|5
189+
6|5|1|5
190+
7|100|7|1
191+
}

0 commit comments

Comments
 (0)