Skip to content

Commit 8549221

Browse files
committed
_first/_last reducers
_first and _last reducers return the first, and respectively the last value in a view group. They can be useful with complex keys, for example, when there are some timestamped entries that may have keys like [device, timestamp], and we would like to get only the latest value by timestamp by grouping by device with `group_level=1`: ``` [dev1, 2025-03-28T00:01:02] : 100.0 [dev1, 2025-03-28T00:03:04] : 91.5 [dev2, 2025-03-29T00:10:15] : 87.6 [dev2, 2025-03-29T00:13:09] : 97.8 ``` We could use the _last reducer with it to return: ``` [dev1] : 91.5 [dev2] : 97.8 ```
1 parent 394f6fd commit 8549221

File tree

4 files changed

+96
-3
lines changed

4 files changed

+96
-3
lines changed

src/couch/src/couch_query_servers.erl

+44-1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ finalize(<<"_approx_count_distinct", _/binary>>, Reduction) ->
100100
{ok, round(couch_hyper:card(Reduction))};
101101
finalize(<<"_stats", _/binary>>, Unpacked) ->
102102
{ok, pack_stats(Unpacked)};
103+
finalize(<<"_first", _/binary>>, {_K, _Id, V}) ->
104+
{ok, V};
105+
finalize(<<"_last", _/binary>>, {_K, _Id, V}) ->
106+
{ok, V};
103107
finalize(_RedSrc, Reduction) ->
104108
{ok, Reduction}.
105109

@@ -202,7 +206,13 @@ builtin_reduce(Re, [<<"_top_", N/binary>> | BuiltinReds], KVs, Acc) ->
202206
builtin_reduce(Re, BuiltinReds, KVs, [Top | Acc]);
203207
builtin_reduce(Re, [<<"_bottom_", N/binary>> | BuiltinReds], KVs, Acc) ->
204208
Bottom = builtin_rank_n(Re, fun rank_fun_bottom/2, binary_to_integer(N), KVs),
205-
builtin_reduce(Re, BuiltinReds, KVs, [Bottom | Acc]).
209+
builtin_reduce(Re, BuiltinReds, KVs, [Bottom | Acc]);
210+
builtin_reduce(Re, [<<"_first", _/binary>> | BuiltinReds], KVs, Acc) ->
211+
First = builtin_first_last(Re, fun builtin_cmp_first/2, KVs),
212+
builtin_reduce(Re, BuiltinReds, KVs, [First | Acc]);
213+
builtin_reduce(Re, [<<"_last", _/binary>> | BuiltinReds], KVs, Acc) ->
214+
Last = builtin_first_last(Re, fun builtin_cmp_last/2, KVs),
215+
builtin_reduce(Re, BuiltinReds, KVs, [Last | Acc]).
206216

207217
builtin_sum_rows([], Acc) ->
208218
Acc;
@@ -406,6 +416,39 @@ rank_fun_top(A, B) ->
406416
rank_fun_bottom(A, B) ->
407417
couch_ejson_compare:less(A, B) =< 0.
408418

419+
builtin_first_last(reduce, _CmpFun, []) ->
420+
[];
421+
builtin_first_last(reduce, CmpFun, [[[Key0, Id0], Value0] | Rest]) ->
422+
lists:foldl(
423+
fun([[K, Id], V], {AccK, AccId, _AccV} = Acc) ->
424+
case CmpFun({K, Id}, {AccK, AccId}) of
425+
true -> {K, Id, V};
426+
false -> Acc
427+
end
428+
end,
429+
{Key0, Id0, Value0},
430+
Rest
431+
);
432+
builtin_first_last(rereduce, _CmpFun, []) ->
433+
[];
434+
builtin_first_last(rereduce, CmpFun, [[_, {K0, Id0, V0}] | Rest]) ->
435+
lists:foldl(
436+
fun([_, {K, Id, V}], {AccK, AccId, _AccV} = Acc) ->
437+
case CmpFun({K, Id}, {AccK, AccId}) of
438+
true -> {K, Id, V};
439+
false -> Acc
440+
end
441+
end,
442+
{K0, Id0, V0},
443+
Rest
444+
).
445+
446+
builtin_cmp_first(A, B) ->
447+
couch_ejson_compare:less_json_ids(A, B).
448+
449+
builtin_cmp_last(A, B) ->
450+
not couch_ejson_compare:less_json_ids(A, B).
451+
409452
% use the function stored in ddoc.validate_doc_update to test an update.
410453
-spec validate_doc_update(Db, DDoc, EditDoc, DiskDoc, Ctx, SecObj) -> ok when
411454
Db :: term(),

src/couch_mrview/src/couch_mrview.erl

+4
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,10 @@ validate(Db, DDoc) ->
217217
ok = check_rank(N);
218218
({_RedName, <<"_bottom_", N/binary>>}) ->
219219
ok = check_rank(N);
220+
({_RedName, <<"_first", _/binary>>}) ->
221+
ok;
222+
({_RedName, <<"_last", _/binary>>}) ->
223+
ok;
220224
({_RedName, <<"_", _/binary>> = Bad}) ->
221225
Msg = ["`", Bad, "` is not a supported reduce function."],
222226
throw({invalid_design_doc, Msg});

src/docs/src/ddocs/ddocs.rst

+15-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,21 @@ Additionally, CouchDB has a set of built-in reduce functions. These are
161161
implemented in Erlang and run inside CouchDB, so they are much faster than the
162162
equivalent JavaScript functions.
163163

164-
.. data:: _top_N
164+
.. data:: _first
165+
166+
.. versionadded:: 3.5
167+
168+
Return the value of the first row in group. For example, for a view like
169+
``[a,1] : x, [a,2] : y``, queried with ``group_level=1``, it would return ``[a]
170+
: x``.
171+
172+
.. data:: _last
173+
174+
.. versionadded:: 3.5
175+
176+
Return the value of the last row in group. For example, for a view like
177+
``[a,1] : x, [a,2] : y``, queried with ``group_level=1``, it would return ``[a]
178+
: y``.
165179

166180
.. versionadded:: 3.5
167181

test/elixir/test/reduce_builtin_test.exs

+33-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ defmodule ReduceBuiltinTest do
6464
:reduce => "_approx_count_distinct"
6565
},
6666
:builtin_top => %{:map => map, :reduce => "_top_3"},
67-
:builtin_bottom => %{:map => map, :reduce => "_bottom_3"}
67+
:builtin_bottom => %{:map => map, :reduce => "_bottom_3"},
68+
:builtin_first => %{:map => map, :reduce => "_first"},
69+
:builtin_last => %{:map => map, :reduce => "_last"},
6870
}
6971
}
7072

@@ -86,6 +88,10 @@ defmodule ReduceBuiltinTest do
8688
assert value == [500, 499, 498]
8789
value = ddoc_url |> query_value("_bottom")
8890
assert value == [1, 2, 3]
91+
value = ddoc_url |> query_value("_first")
92+
assert value == 1
93+
value = ddoc_url |> query_value("_last")
94+
assert value == 500
8995

9096
value = ddoc_url |> query_value("_sum", %{startkey: 4, endkey: 4})
9197
assert value == 8
@@ -97,6 +103,10 @@ defmodule ReduceBuiltinTest do
97103
assert value == [4]
98104
value = ddoc_url |> query_value("_bottom", %{startkey: 4, endkey: 4})
99105
assert value == [4]
106+
value = ddoc_url |> query_value("_first", %{startkey: 4, endkey: 4})
107+
assert value == 4
108+
value = ddoc_url |> query_value("_last", %{startkey: 4, endkey: 4})
109+
assert value == 4
100110

101111
value = ddoc_url |> query_value("_sum", %{startkey: 4, endkey: 5})
102112
assert value == 18
@@ -108,6 +118,10 @@ defmodule ReduceBuiltinTest do
108118
assert value == [5, 4]
109119
value = ddoc_url |> query_value("_bottom", %{startkey: 4, endkey: 5})
110120
assert value == [4, 5]
121+
value = ddoc_url |> query_value("_first", %{startkey: 4, endkey: 5})
122+
assert value == 4
123+
value = ddoc_url |> query_value("_last", %{startkey: 4, endkey: 5})
124+
assert value == 5
111125

112126
value = ddoc_url |> query_value("_sum", %{startkey: 4, endkey: 6})
113127
assert value == 30
@@ -119,6 +133,10 @@ defmodule ReduceBuiltinTest do
119133
assert value == [6, 5, 4]
120134
value = ddoc_url |> query_value("_bottom", %{startkey: 4, endkey: 6})
121135
assert value == [4, 5, 6]
136+
value = ddoc_url |> query_value("_first", %{startkey: 4, endkey: 6})
137+
assert value == 4
138+
value = ddoc_url |> query_value("_last", %{startkey: 4, endkey: 6})
139+
assert value == 6
122140

123141
assert [row0, row1, row2] = ddoc_url |> query_rows("_sum", %{group: true, limit: 3})
124142
assert row0["value"] == 2
@@ -146,6 +164,20 @@ defmodule ReduceBuiltinTest do
146164
assert row1["value"] == [2]
147165
assert row2["value"] == [3]
148166

167+
assert [row0, row1, row2] =
168+
ddoc_url |> query_rows("_first", %{group: true, limit: 3})
169+
170+
assert row0["value"] == 1
171+
assert row1["value"] == 2
172+
assert row2["value"] == 3
173+
174+
assert [row0, row1, row2] =
175+
ddoc_url |> query_rows("_last", %{group: true, limit: 3})
176+
177+
assert row0["value"] == 1
178+
assert row1["value"] == 2
179+
assert row2["value"] == 3
180+
149181
1..div(500, 2)
150182
|> Enum.take_every(30)
151183
|> Enum.each(fn i ->

0 commit comments

Comments
 (0)