Packages
rocksdb
2.4.0
3.1.1
3.1.0
3.0.0
2.6.2
2.6.1
retired
2.6.0
retired
2.5.0
2.4.1
2.4.0
2.3.0
2.2.0
2.1.0
2.0.0
1.9.0
1.8.0
1.7.0
1.6.0
1.5.1
1.5.0
1.4.0
1.3.2
1.3.1
1.3.0
1.2.0
1.1.1
1.1.0
1.0.0
0.26.2
0.26.1
0.26.0
0.25.0
0.24.0
0.23.3
0.23.2
0.23.1
0.23.0
0.22.0
0.21.0
0.20.1
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.0
0.14.0
0.13.1
0.13.0
0.12.0
0.11.0
0.10.0
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.1
0.7.0
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
RocksDB for Erlang
Current section
Files
Jump to
Current section
Files
test/posting_list.erl
%% Copyright (c) 2016-2026 Benoit Chesneau
%%
%% This file is provided to you under the Apache License,
%% Version 2.0 (the "License"); you may not use this file
%% except in compliance with the License. You may obtain
%% a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing,
%% software distributed under the License is distributed on an
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%% KIND, either express or implied. See the License for the
%% specific language governing permissions and limitations
%% under the License.
-module(posting_list).
-compile([export_all, nowarn_export_all]).
-include_lib("eunit/include/eunit.hrl").
%% ===================================================================
%% Basic Tests
%% ===================================================================
posting_list_add_test() ->
DbPath = "posting_list_add.test",
rocksdb_test_util:rm_rf(DbPath),
{ok, Db} = rocksdb:open(DbPath, [
{create_if_missing, true},
{merge_operator, posting_list_merge_operator}
]),
%% Add keys to posting list
ok = rocksdb:merge(Db, <<"term">>, {posting_add, <<"doc1">>}, []),
ok = rocksdb:merge(Db, <<"term">>, {posting_add, <<"doc2">>}, []),
ok = rocksdb:merge(Db, <<"term">>, {posting_add, <<"doc3">>}, []),
%% Get and verify
{ok, Bin} = rocksdb:get(Db, <<"term">>, []),
Keys = rocksdb:posting_list_keys(Bin),
?assert(lists:member(<<"doc1">>, Keys)),
?assert(lists:member(<<"doc2">>, Keys)),
?assert(lists:member(<<"doc3">>, Keys)),
?assertEqual(3, rocksdb:posting_list_count(Bin)),
ok = rocksdb:close(Db),
rocksdb:destroy(DbPath, []),
rocksdb_test_util:rm_rf(DbPath).
posting_list_delete_test() ->
DbPath = "posting_list_delete.test",
rocksdb_test_util:rm_rf(DbPath),
{ok, Db} = rocksdb:open(DbPath, [
{create_if_missing, true},
{merge_operator, posting_list_merge_operator}
]),
%% Add keys
ok = rocksdb:merge(Db, <<"term">>, {posting_add, <<"doc1">>}, []),
ok = rocksdb:merge(Db, <<"term">>, {posting_add, <<"doc2">>}, []),
ok = rocksdb:merge(Db, <<"term">>, {posting_add, <<"doc3">>}, []),
%% Delete doc2
ok = rocksdb:merge(Db, <<"term">>, {posting_delete, <<"doc2">>}, []),
%% Get and verify
{ok, Bin} = rocksdb:get(Db, <<"term">>, []),
Keys = rocksdb:posting_list_keys(Bin),
?assert(lists:member(<<"doc1">>, Keys)),
?assertNot(lists:member(<<"doc2">>, Keys)), % Deleted
?assert(lists:member(<<"doc3">>, Keys)),
?assertEqual(2, rocksdb:posting_list_count(Bin)),
ok = rocksdb:close(Db),
rocksdb:destroy(DbPath, []),
rocksdb_test_util:rm_rf(DbPath).
posting_list_compaction_test() ->
%% Test that merge operator cleans up tombstones during merge
%% Tombstones are removed during reads (FullMergeV2) and compaction (PartialMergeMulti)
DbPath = "posting_list_tombstones.test",
rocksdb_test_util:rm_rf(DbPath),
{ok, Db} = rocksdb:open(DbPath, [
{create_if_missing, true},
{write_buffer_size, 64 * 1024},
{level0_file_num_compaction_trigger, 1},
{merge_operator, posting_list_merge_operator}
]),
%% Create first SST file with base value
ok = rocksdb:put(Db, <<"term">>, <<>>, []),
lists:foreach(fun(N) ->
PadKey = iolist_to_binary(["padding_a", integer_to_list(N)]),
PadValue = binary:copy(<<"x">>, 1000),
ok = rocksdb:put(Db, PadKey, PadValue, [])
end, lists:seq(1, 50)),
ok = rocksdb:flush(Db, []),
%% Create second SST file with merge operands
ok = rocksdb:merge(Db, <<"term">>, {posting_add, <<"doc1">>}, []),
ok = rocksdb:merge(Db, <<"term">>, {posting_add, <<"doc2">>}, []),
ok = rocksdb:merge(Db, <<"term">>, {posting_delete, <<"doc1">>}, []),
lists:foreach(fun(N) ->
PadKey = iolist_to_binary(["padding_b", integer_to_list(N)]),
PadValue = binary:copy(<<"y">>, 1000),
ok = rocksdb:put(Db, PadKey, PadValue, [])
end, lists:seq(1, 50)),
ok = rocksdb:flush(Db, []),
%% During read, the merge operator cleans up tombstones (FullMergeV2)
%% Only doc2 should be present (doc1 was added then deleted)
{ok, Bin1} = rocksdb:get(Db, <<"term">>, []),
Entries1 = rocksdb:posting_list_decode(Bin1),
?assertEqual([{<<"doc2">>, false}], Entries1),
%% Force compaction - PartialMergeMulti consolidates operands
ok = rocksdb:compact_range(Db, undefined, undefined, [{bottommost_level_compaction, force}]),
%% After compaction, result should still be only doc2
{ok, Bin2} = rocksdb:get(Db, <<"term">>, []),
Entries2 = rocksdb:posting_list_decode(Bin2),
?assertEqual([{<<"doc2">>, false}], Entries2),
ok = rocksdb:close(Db),
rocksdb_test_util:rm_rf(DbPath).
%% ===================================================================
%% Helper Function Tests
%% ===================================================================
posting_list_decode_test() ->
%% Build a posting list binary manually
Bin = <<3:32/big, 0, "foo", 3:32/big, 1, "bar", 3:32/big, 0, "baz">>,
Entries = rocksdb:posting_list_decode(Bin),
?assertEqual([{<<"foo">>, false}, {<<"bar">>, true}, {<<"baz">>, false}], Entries).
posting_list_fold_test() ->
Bin = <<3:32/big, 0, "foo", 3:32/big, 1, "bar", 3:32/big, 0, "baz">>,
Count = rocksdb:posting_list_fold(
fun(_Key, _IsTombstone, Acc) -> Acc + 1 end,
0,
Bin
),
?assertEqual(3, Count).
posting_list_keys_test() ->
%% foo (normal), bar (tombstone), baz (normal)
Bin = <<3:32/big, 0, "foo", 3:32/big, 1, "bar", 3:32/big, 0, "baz">>,
Keys = rocksdb:posting_list_keys(Bin),
?assert(lists:member(<<"foo">>, Keys)),
?assertNot(lists:member(<<"bar">>, Keys)), % Tombstoned
?assert(lists:member(<<"baz">>, Keys)),
?assertEqual(2, length(Keys)).
posting_list_contains_test() ->
%% foo (normal), bar (normal), bar (tombstone) - bar is tombstoned
Bin = <<3:32/big, 0, "foo", 3:32/big, 0, "bar", 3:32/big, 1, "bar">>,
?assertEqual(true, rocksdb:posting_list_contains(Bin, <<"foo">>)),
?assertEqual(false, rocksdb:posting_list_contains(Bin, <<"bar">>)), % Tombstoned
?assertEqual(false, rocksdb:posting_list_contains(Bin, <<"baz">>)). % Not found
posting_list_find_test() ->
%% foo (normal), bar (normal), bar (tombstone) - bar is tombstoned
Bin = <<3:32/big, 0, "foo", 3:32/big, 0, "bar", 3:32/big, 1, "bar">>,
?assertEqual({ok, false}, rocksdb:posting_list_find(Bin, <<"foo">>)),
?assertEqual({ok, true}, rocksdb:posting_list_find(Bin, <<"bar">>)), % Tombstoned
?assertEqual(not_found, rocksdb:posting_list_find(Bin, <<"baz">>)).
posting_list_count_test() ->
Bin = <<3:32/big, 0, "foo", 3:32/big, 1, "bar", 3:32/big, 0, "baz">>,
?assertEqual(2, rocksdb:posting_list_count(Bin)). % bar is tombstoned
posting_list_to_map_test() ->
Bin = <<3:32/big, 0, "foo", 3:32/big, 1, "bar", 3:32/big, 0, "baz">>,
Map = rocksdb:posting_list_to_map(Bin),
?assertEqual(active, maps:get(<<"foo">>, Map)),
?assertEqual(tombstone, maps:get(<<"bar">>, Map)),
?assertEqual(active, maps:get(<<"baz">>, Map)).
posting_list_empty_test() ->
Bin = <<>>,
?assertEqual([], rocksdb:posting_list_decode(Bin)),
?assertEqual([], rocksdb:posting_list_keys(Bin)),
?assertEqual(0, rocksdb:posting_list_count(Bin)),
?assertEqual(#{}, rocksdb:posting_list_to_map(Bin)).
%% ===================================================================
%% Duplicate Keys Tests
%% ===================================================================
posting_list_duplicate_keys_test() ->
%% Add same key multiple times, last occurrence wins
%% doc1 (add), doc1 (delete), doc1 (add) -> doc1 is active
Bin = <<4:32/big, 0, "doc1", 4:32/big, 1, "doc1", 4:32/big, 0, "doc1">>,
?assertEqual(true, rocksdb:posting_list_contains(Bin, <<"doc1">>)),
?assertEqual({ok, false}, rocksdb:posting_list_find(Bin, <<"doc1">>)),
?assertEqual(1, rocksdb:posting_list_count(Bin)).
posting_list_duplicate_keys_tombstoned_test() ->
%% doc1 (add), doc1 (add), doc1 (delete) -> doc1 is tombstoned
Bin = <<4:32/big, 0, "doc1", 4:32/big, 0, "doc1", 4:32/big, 1, "doc1">>,
?assertEqual(false, rocksdb:posting_list_contains(Bin, <<"doc1">>)),
?assertEqual({ok, true}, rocksdb:posting_list_find(Bin, <<"doc1">>)),
?assertEqual(0, rocksdb:posting_list_count(Bin)).