You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by va...@apache.org on 2025/04/02 14:09:37 UTC
(couchdb) 01/01: _first/_last reducers
This is an automated email from the ASF dual-hosted git repository.
vatamane pushed a commit to branch first-last-reducer
in repository https://gitbox.apache.org/repos/asf/couchdb.git
commit d7e25939908689440b154b1fe8109623a9518f66
Author: Nick Vatamaniuc <va...@gmail.com>
AuthorDate: Fri Mar 28 19:52:25 2025 -0400
_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 like key = `[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 to return:
```
[dev1] : 91.5
[dev2] : 97.8
```
---
src/couch/src/couch_query_servers.erl | 47 ++++++++++++++++++++++++++++----
src/couch_mrview/src/couch_mrview.erl | 4 +++
src/docs/src/ddocs/ddocs.rst | 16 ++++++++++-
test/elixir/test/reduce_builtin_test.exs | 34 ++++++++++++++++++++++-
4 files changed, 94 insertions(+), 7 deletions(-)
diff --git a/src/couch/src/couch_query_servers.erl b/src/couch/src/couch_query_servers.erl
index fcf62eb83..d209a728d 100644
--- a/src/couch/src/couch_query_servers.erl
+++ b/src/couch/src/couch_query_servers.erl
@@ -100,6 +100,10 @@ finalize(<<"_approx_count_distinct", _/binary>>, Reduction) ->
{ok, round(couch_hyper:card(Reduction))};
finalize(<<"_stats", _/binary>>, Unpacked) ->
{ok, pack_stats(Unpacked)};
+finalize(<<"_first", _/binary>>, {_K, _Id, V}) ->
+ {ok, V};
+finalize(<<"_last", _/binary>>, {_K, _Id, V}) ->
+ {ok, V};
finalize(_RedSrc, Reduction) ->
{ok, Reduction}.
@@ -198,11 +202,17 @@ builtin_reduce(Re, [<<"_approx_count_distinct", _/binary>> | BuiltinReds], KVs,
Distinct = approx_count_distinct(Re, KVs),
builtin_reduce(Re, BuiltinReds, KVs, [Distinct | Acc]);
builtin_reduce(Re, [<<"_top_", N/binary>> | BuiltinReds], KVs, Acc) ->
- Top = builtin_rank_n(Re, fun rank_fun_top/2, binary_to_integer(N), KVs),
+ Top = builtin_rank_n(Re, fun cmp_gt_eq/2, binary_to_integer(N), KVs),
builtin_reduce(Re, BuiltinReds, KVs, [Top | Acc]);
builtin_reduce(Re, [<<"_bottom_", N/binary>> | BuiltinReds], KVs, Acc) ->
- Bottom = builtin_rank_n(Re, fun rank_fun_bottom/2, binary_to_integer(N), KVs),
- builtin_reduce(Re, BuiltinReds, KVs, [Bottom | Acc]).
+ Bottom = builtin_rank_n(Re, fun cmp_lt_eq/2, binary_to_integer(N), KVs),
+ builtin_reduce(Re, BuiltinReds, KVs, [Bottom | Acc]);
+builtin_reduce(Re, [<<"_first", _/binary>> | BuiltinReds], KVs, Acc) ->
+ First = builtin_first_last(Re, fun cmp_lt_eq/2, KVs),
+ builtin_reduce(Re, BuiltinReds, KVs, [First | Acc]);
+builtin_reduce(Re, [<<"_last", _/binary>> | BuiltinReds], KVs, Acc) ->
+ Last = builtin_first_last(Re, fun cmp_gt_eq/2, KVs),
+ builtin_reduce(Re, BuiltinReds, KVs, [Last | Acc]).
builtin_sum_rows([], Acc) ->
Acc;
@@ -400,12 +410,39 @@ builtin_rank_n(rereduce, RankFun, N, [[_, KVs0] | Rest]) when is_integer(N), N >
Fun = fun([_, KVs], Acc) -> lists:umerge(RankFun, Acc, KVs) end,
lists:sublist(lists:foldl(Fun, KVs0, Rest), N).
-rank_fun_top(A, B) ->
+cmp_gt_eq(A, B) ->
couch_ejson_compare:less(A, B) >= 0.
-rank_fun_bottom(A, B) ->
+cmp_lt_eq(A, B) ->
couch_ejson_compare:less(A, B) =< 0.
+builtin_first_last(reduce, _CmpFun, []) ->
+ [];
+builtin_first_last(reduce, CmpFun, [[[Key0, Id0], Value0] | Rest]) ->
+ lists:foldl(
+ fun([[K, Id], V], {AccK, AccId, _AccV} = Acc) ->
+ case CmpFun([K, Id], [AccK, AccId]) of
+ true -> {K, Id, V};
+ false -> Acc
+ end
+ end,
+ {Key0, Id0, Value0},
+ Rest
+ );
+builtin_first_last(rereduce, _CmpFun, []) ->
+ [];
+builtin_first_last(rereduce, CmpFun, [[_, {K0, Id0, V0}] | Rest]) ->
+ lists:foldl(
+ fun([_, {K, Id, V}], {AccK, AccId, _AccV} = Acc) ->
+ case CmpFun([K, Id], [AccK, AccId]) of
+ true -> {K, Id, V};
+ false -> Acc
+ end
+ end,
+ {K0, Id0, V0},
+ Rest
+ ).
+
% use the function stored in ddoc.validate_doc_update to test an update.
-spec validate_doc_update(Db, DDoc, EditDoc, DiskDoc, Ctx, SecObj) -> ok when
Db :: term(),
diff --git a/src/couch_mrview/src/couch_mrview.erl b/src/couch_mrview/src/couch_mrview.erl
index c11a91e79..b31938f74 100644
--- a/src/couch_mrview/src/couch_mrview.erl
+++ b/src/couch_mrview/src/couch_mrview.erl
@@ -217,6 +217,10 @@ validate(Db, DDoc) ->
ok = check_rank(N);
({_RedName, <<"_bottom_", N/binary>>}) ->
ok = check_rank(N);
+ ({_RedName, <<"_first", _/binary>>}) ->
+ ok;
+ ({_RedName, <<"_last", _/binary>>}) ->
+ ok;
({_RedName, <<"_", _/binary>> = Bad}) ->
Msg = ["`", Bad, "` is not a supported reduce function."],
throw({invalid_design_doc, Msg});
diff --git a/src/docs/src/ddocs/ddocs.rst b/src/docs/src/ddocs/ddocs.rst
index 4f007c36d..2b57bbc99 100644
--- a/src/docs/src/ddocs/ddocs.rst
+++ b/src/docs/src/ddocs/ddocs.rst
@@ -161,7 +161,21 @@ Additionally, CouchDB has a set of built-in reduce functions. These are
implemented in Erlang and run inside CouchDB, so they are much faster than the
equivalent JavaScript functions.
-.. data:: _top_N
+.. data:: _first
+
+.. versionadded:: 3.5
+
+Return the value of the first row in group. For example, for a view like
+``[a,1] : x, [a,2] : y``, queried with ``group_level=1``, it would return ``[a]
+: x``.
+
+.. data:: _last
+
+.. versionadded:: 3.5
+
+Return the value of the last row in group. For example, for a view like
+``[a,1] : x, [a,2] : y``, queried with ``group_level=1``, it would return ``[a]
+: y``.
.. versionadded:: 3.5
diff --git a/test/elixir/test/reduce_builtin_test.exs b/test/elixir/test/reduce_builtin_test.exs
index 903fc0d2d..98c872f11 100644
--- a/test/elixir/test/reduce_builtin_test.exs
+++ b/test/elixir/test/reduce_builtin_test.exs
@@ -64,7 +64,9 @@ defmodule ReduceBuiltinTest do
:reduce => "_approx_count_distinct"
},
:builtin_top => %{:map => map, :reduce => "_top_3"},
- :builtin_bottom => %{:map => map, :reduce => "_bottom_3"}
+ :builtin_bottom => %{:map => map, :reduce => "_bottom_3"},
+ :builtin_first => %{:map => map, :reduce => "_first"},
+ :builtin_last => %{:map => map, :reduce => "_last"},
}
}
@@ -86,6 +88,10 @@ defmodule ReduceBuiltinTest do
assert value == [500, 499, 498]
value = ddoc_url |> query_value("_bottom")
assert value == [1, 2, 3]
+ value = ddoc_url |> query_value("_first")
+ assert value == 1
+ value = ddoc_url |> query_value("_last")
+ assert value == 500
value = ddoc_url |> query_value("_sum", %{startkey: 4, endkey: 4})
assert value == 8
@@ -97,6 +103,10 @@ defmodule ReduceBuiltinTest do
assert value == [4]
value = ddoc_url |> query_value("_bottom", %{startkey: 4, endkey: 4})
assert value == [4]
+ value = ddoc_url |> query_value("_first", %{startkey: 4, endkey: 4})
+ assert value == 4
+ value = ddoc_url |> query_value("_last", %{startkey: 4, endkey: 4})
+ assert value == 4
value = ddoc_url |> query_value("_sum", %{startkey: 4, endkey: 5})
assert value == 18
@@ -108,6 +118,10 @@ defmodule ReduceBuiltinTest do
assert value == [5, 4]
value = ddoc_url |> query_value("_bottom", %{startkey: 4, endkey: 5})
assert value == [4, 5]
+ value = ddoc_url |> query_value("_first", %{startkey: 4, endkey: 5})
+ assert value == 4
+ value = ddoc_url |> query_value("_last", %{startkey: 4, endkey: 5})
+ assert value == 5
value = ddoc_url |> query_value("_sum", %{startkey: 4, endkey: 6})
assert value == 30
@@ -119,6 +133,10 @@ defmodule ReduceBuiltinTest do
assert value == [6, 5, 4]
value = ddoc_url |> query_value("_bottom", %{startkey: 4, endkey: 6})
assert value == [4, 5, 6]
+ value = ddoc_url |> query_value("_first", %{startkey: 4, endkey: 6})
+ assert value == 4
+ value = ddoc_url |> query_value("_last", %{startkey: 4, endkey: 6})
+ assert value == 6
assert [row0, row1, row2] = ddoc_url |> query_rows("_sum", %{group: true, limit: 3})
assert row0["value"] == 2
@@ -146,6 +164,20 @@ defmodule ReduceBuiltinTest do
assert row1["value"] == [2]
assert row2["value"] == [3]
+ assert [row0, row1, row2] =
+ ddoc_url |> query_rows("_first", %{group: true, limit: 3})
+
+ assert row0["value"] == 1
+ assert row1["value"] == 2
+ assert row2["value"] == 3
+
+ assert [row0, row1, row2] =
+ ddoc_url |> query_rows("_last", %{group: true, limit: 3})
+
+ assert row0["value"] == 1
+ assert row1["value"] == 2
+ assert row2["value"] == 3
+
1..div(500, 2)
|> Enum.take_every(30)
|> Enum.each(fn i ->