Skip to content

Commit

Permalink
Add ets:update_counter
Browse files Browse the repository at this point in the history
Signed-off-by: Tomasz Sobkiewicz <[email protected]>
  • Loading branch information
TheSobkiewicz committed Jan 9, 2025
1 parent 76774f0 commit 9ed442e
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 3 deletions.
42 changes: 41 additions & 1 deletion libs/estdlib/src/ets.erl
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
insert/2,
lookup/2,
lookup_element/3,
delete/2
delete/2,
update_counter/3,
update_counter/4
]).

-export_type([
Expand Down Expand Up @@ -101,3 +103,41 @@ lookup_element(_Table, _Key, _Pos) ->
-spec delete(Table :: table(), Key :: term()) -> true.
delete(_Table, _Key) ->
erlang:nif_error(undefined).

%%-----------------------------------------------------------------------------
%% @param Table a reference to the ets table
%% @param Key the key used to look up the entry expecting to contain a tuple of integers or a single integer
%% @param Params the increment value or a tuple {Pos, Increment} or {Pos, Increment, Treshold, SetValue},
%% where Pos is an integer (1-based index) specifying the position in the tuple to increment. Value is clamped to SetValue if it exceeds Threshold after update.
%% @returns the updated element's value after performing the increment, or the default value if applicable
%% @doc Updates a counter value at Key in the table. If Params is a single integer, it increments the direct integer value at Key or the first integer in a tuple. If Params is a tuple {Pos, Increment}, it increments the integer at the specified position Pos in the tuple stored at Key.
%% @end
%%-----------------------------------------------------------------------------
-spec update_counter(
Table :: table(),
Key :: term(),
Params ::
integer() | {pos_integer(), integer()} | {pos_integer(), integer(), integer(), integer()}
) -> integer().
update_counter(_Table, _Key, _Params) ->
erlang:nif_error(undefined).

%%-----------------------------------------------------------------------------
%% @param Table a reference to the ets table
%% @param Key the key used to look up the entry expecting to contain a tuple of integers or a single integer
%% @param Params the increment value or a tuple {Pos, Increment} or {Pos, Increment, Treshold, SetValue},
%% where Pos is an integer (1-based index) specifying the position in the tuple to increment. If after incrementation value exceeds the Treshold, it is set to SetValue.
%% @param Default the default value used if the entry at Key doesn't exist or doesn't contain a valid tuple with a sufficient size or integer at Pos
%% @returns the updated element's value after performing the increment, or the default value if applicable
%% @doc Updates a counter value at Key in the table. If Params is a single integer, it increments the direct integer value at Key or the first integer in a tuple. If Params is a tuple {Pos, Increment}, it increments the integer at the specified position Pos in the tuple stored at Key. If the needed element does not exist, uses Default value as a fallback.
%% @end
%%-----------------------------------------------------------------------------
-spec update_counter(
Table :: table(),
Key :: term(),
Params ::
integer() | {pos_integer(), integer()} | {pos_integer(), integer(), integer(), integer()},
Default :: integer()
) -> integer().
update_counter(_Table, _Key, _Params, _Default) ->
erlang:nif_error(undefined).
120 changes: 120 additions & 0 deletions src/libAtomVM/ets.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include "ets_hashtable.h"
#include "list.h"
#include "memory.h"
#include "overflow_helpers.h"
#include "term.h"

#ifndef AVM_NO_SMP
Expand Down Expand Up @@ -428,3 +429,122 @@ EtsErrorCode ets_delete(term name_or_ref, term key, term *ret, Context *ctx)

return EtsOk;
}

static bool operation_to_tuple4(term operation, term *position, term *increment, term *threshold, term *set_value)
{
if (term_is_integer(operation)) {
*increment = operation;
*position = term_from_int(2);
*threshold = term_invalid_term();
*set_value = term_invalid_term();
return true;
}

if (!term_is_tuple(operation)) {
return false;
}
int n = term_get_tuple_arity(operation);
if (n != 2 && n != 4) {
return false;
}

term pos = term_get_tuple_element(operation, 0);
term incr = term_get_tuple_element(operation, 1);
if (!term_is_integer(pos) || !term_is_integer(incr)) {
return false;
}

if (n == 2) {
*position = pos;
*increment = incr;
*threshold = term_invalid_term();
*set_value = term_invalid_term();
return true;
}

term tresh = term_get_tuple_element(operation, 2);
term set_val = term_get_tuple_element(operation, 3);
if (!term_is_integer(tresh) || !term_is_integer(set_val)) {
return false;
}

*position = pos;
*increment = incr;
*threshold = tresh;
*set_value = set_val;
return true;
}

EtsErrorCode ets_update_counter(term ref, term key, term operation, term default_value, term *ret, Context *ctx)
{
struct EtsTable *ets_table = term_is_atom(ref) ? ets_get_table_by_name(&ctx->global->ets, ref, TableAccessWrite) : ets_get_table_by_ref(&ctx->global->ets, term_to_ref_ticks(ref), TableAccessWrite);
if (IS_NULL_PTR(ets_table)) {
return EtsTableNotFound;
}

term list = term_invalid_term();
EtsErrorCode result = ets_table_lookup(ets_table, key, &list, ctx);
if (result != EtsOk) {
SMP_UNLOCK(ets_table);
return result;
}

term to_insert;
if (term_is_nil(list)) {
if (term_is_invalid_term(default_value)) {
SMP_UNLOCK(ets_table);
return EtsBadEntry;
}
to_insert = default_value;
} else {
to_insert = term_get_list_head(list);
}

if (!(term_is_tuple(to_insert))) {
SMP_UNLOCK(ets_table);
return EtsBadEntry;
}
term position_term, increment_term, threshold_term, set_value_term;

if (!operation_to_tuple4(operation, &position_term, &increment_term, &threshold_term, &set_value_term)) {
SMP_UNLOCK(ets_table);
return EtsBadEntry;
}
int arity = term_get_tuple_arity(to_insert);
int position = term_to_int(position_term) - 1;
if (arity <= position || position < 1) {
SMP_UNLOCK(ets_table);
return EtsBadEntry;
}

term elem = term_get_tuple_element(to_insert, position);
if (!term_is_integer(elem)) {
SMP_UNLOCK(ets_table);
return EtsBadEntry;
}
int increment = term_to_int(increment_term);
int elem_value;
if (BUILTIN_ADD_OVERFLOW_INT(increment, term_to_int(elem), &elem_value)) {
SMP_UNLOCK(ets_table);
return EtsOverlfow;
}
if (!term_is_invalid_term(threshold_term) && !term_is_invalid_term(set_value_term)) {
int threshold = term_to_int(threshold_term);
int set_value = term_to_int(set_value_term);

if (increment >= 0 && elem_value > threshold) {
elem_value = set_value;
} else if (increment < 0 && elem_value < threshold) {
elem_value = set_value;
}
}

elem = term_from_int(elem_value);
term_put_tuple_element(to_insert, position, elem);
EtsErrorCode insert_result = ets_table_insert(ets_table, to_insert, ctx);
if (insert_result == EtsOk) {
*ret = elem;
}
SMP_UNLOCK(ets_table);
return insert_result;
}
5 changes: 3 additions & 2 deletions src/libAtomVM/ets.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ typedef enum EtsErrorCode
EtsBadEntry,
EtsAllocationFailure,
EtsEntryNotFound,
EtsBadPosition
EtsBadPosition,
EtsOverlfow
} EtsErrorCode;
struct Ets
{
Expand All @@ -77,7 +78,7 @@ EtsErrorCode ets_insert(term ref, term entry, Context *ctx);
EtsErrorCode ets_lookup(term ref, term key, term *ret, Context *ctx);
EtsErrorCode ets_lookup_element(term ref, term key, size_t pos, term *ret, Context *ctx);
EtsErrorCode ets_delete(term ref, term key, term *ret, Context *ctx);

EtsErrorCode ets_update_counter(term ref, term key, term value, term pos, term *ret, Context *ctx);
#ifdef __cplusplus
}
#endif
Expand Down
38 changes: 38 additions & 0 deletions src/libAtomVM/nifs.c
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ static term nif_ets_insert(Context *ctx, int argc, term argv[]);
static term nif_ets_lookup(Context *ctx, int argc, term argv[]);
static term nif_ets_lookup_element(Context *ctx, int argc, term argv[]);
static term nif_ets_delete(Context *ctx, int argc, term argv[]);
static term nif_ets_update_counter(Context *ctx, int argc, term argv[]);
static term nif_erlang_pid_to_list(Context *ctx, int argc, term argv[]);
static term nif_erlang_ref_to_list(Context *ctx, int argc, term argv[]);
static term nif_erlang_fun_to_list(Context *ctx, int argc, term argv[]);
Expand Down Expand Up @@ -697,6 +698,12 @@ static const struct Nif ets_delete_nif =
.nif_ptr = nif_ets_delete
};

static const struct Nif ets_update_counter_nif =
{
.base.type = NIFFunctionType,
.nif_ptr = nif_ets_update_counter
};

static const struct Nif atomvm_add_avm_pack_binary_nif =
{
.base.type = NIFFunctionType,
Expand Down Expand Up @@ -3415,6 +3422,37 @@ static term nif_ets_delete(Context *ctx, int argc, term argv[])
}
}

static term nif_ets_update_counter(Context *ctx, int argc, term argv[])
{
term ref = argv[0];
VALIDATE_VALUE(ref, is_ets_table_id);

term key = argv[1];
term operation = argv[2];
term default_value = term_invalid_term();
if (argc == 4) {
default_value = argv[3];
VALIDATE_VALUE(default_value, term_is_tuple);
term_put_tuple_element(default_value, 0, key);
}
term ret = term_invalid_term();
EtsErrorCode result = ets_update_counter(ref, key, operation, default_value, &ret, ctx);
switch (result) {
case EtsOk:
return ret;
case EtsTableNotFound:
case EtsPermissionDenied:
case EtsBadEntry:
RAISE_ERROR(BADARG_ATOM);
case EtsAllocationFailure:
RAISE_ERROR(MEMORY_ATOM);
case EtsOverlfow:
RAISE_ERROR(OVERFLOW_ATOM);
default:
AVM_ABORT();
}
}

static term nif_erts_debug_flat_size(Context *ctx, int argc, term argv[])
{
UNUSED(ctx);
Expand Down
2 changes: 2 additions & 0 deletions src/libAtomVM/nifs.gperf
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ ets:insert/2, &ets_insert_nif
ets:lookup/2, &ets_lookup_nif
ets:lookup_element/3, &ets_lookup_element_nif
ets:delete/2, &ets_delete_nif
ets:update_counter/3, &ets_update_counter_nif
ets:update_counter/4, &ets_update_counter_nif
atomvm:add_avm_pack_binary/2, &atomvm_add_avm_pack_binary_nif
atomvm:add_avm_pack_file/2, &atomvm_add_avm_pack_file_nif
atomvm:close_avm_pack/2, &atomvm_close_avm_pack_nif
Expand Down
19 changes: 19 additions & 0 deletions tests/erlang_tests/test_ets.erl
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ start() ->
ok = test_public_access(),
ok = test_lookup_element(),
ok = test_insert_list(),
ok = test_update_counter(),
0.

test_basic() ->
Expand Down Expand Up @@ -362,3 +363,21 @@ test_insert_list() ->
expect_failure(fun() -> ets:insert(Tid, [{foo, tapas} | {patat, patat}]) end),
expect_failure(fun() -> ets:insert(Tid, [{}]) end),
ok.

test_update_counter() ->
Tid = ets:new(test_lookup_element, []),
true = ets:insert(Tid, {foo, 1, 2, 3}),
3 = ets:update_counter(Tid, foo, 2),
expect_failure(fun() -> ets:update_counter(Tid, tapas, 2) end),
5 = ets:update_counter(Tid, tapas, 2, {batat, 3}),
[] = ets:lookup(Tid, batat),
[{tapas, 5}] = ets:lookup(Tid, tapas),
0 = ets:update_counter(Tid, foo, {3, -2}),
expect_failure(fun() -> ets:update_counter(Tid, foo, {-3, -2}) end),
expect_failure(fun() -> ets:update_counter(Tid, foo, {30, -2}) end),
expect_failure(fun() -> ets:update_counter(Tid, patatas, {3, -2}, {cow, 1}) end),
0 = ets:update_counter(Tid, patatas, {3, -2}, {cow, 1, 2, 3}),
0 = ets:update_counter(Tid, patatas, {3, -2, 0, 0}),
10 = ets:update_counter(Tid, patatas, {3, 10, 10, 0}),
0 = ets:update_counter(Tid, patatas, {3, 10, 10, 0}),
ok.

0 comments on commit 9ed442e

Please sign in to comment.