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 ->