Skip to content

Commit c642575

Browse files
authored
Support sub-key queries (#457)
* Support sub-key queries Also requires a refactoring of types. In head-only mode - the metadata in the ledger is just the value, and the value can be anything. So metadata() definition needs to reflect that. There are then issues with appdefined functions for extracting metadata. In theory an appdefined function could extract some unsopprted type. So made explicit that the appdefined function must extract std_metadata() as metadata - otherwise functionality will not work. This means that if it is an object key, that is not a ?HEAD key, then the Metadata must be a tuple (of either Riak or Standard type). * Fix coverage issues
1 parent 98cdb4d commit c642575

7 files changed

+138
-60
lines changed

src/leveled_bookie.erl

+2-2
Original file line numberDiff line numberDiff line change
@@ -2438,13 +2438,13 @@ recalcfor_ledgercache(
24382438
not_present;
24392439
{LK, LV} ->
24402440
case leveled_codec:get_metadata(LV) of
2441-
MDO when MDO =/= null ->
2441+
MDO when is_tuple(MDO) ->
24422442
MDO
24432443
end
24442444
end,
24452445
UpdMetadata =
24462446
case leveled_codec:get_metadata(MetaValue) of
2447-
MDU when MDU =/= null ->
2447+
MDU when is_tuple(MDU) ->
24482448
MDU
24492449
end,
24502450
IdxSpecs =

src/leveled_codec.erl

+15-14
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,9 @@
7373
-type segment_hash() ::
7474
% hash of the key to an aae segment - to be used in ledger filters
7575
{integer(), integer()}|no_lookup.
76+
-type head_value() :: any().
7677
-type metadata() ::
77-
tuple()|null. % null for empty metadata
78+
tuple()|null|head_value(). % null for empty metadata
7879
-type last_moddate() ::
7980
% modified date as determined by the object (not this store)
8081
% if the object has siblings in the store will be the maximum of those
@@ -177,7 +178,8 @@
177178
regular_expression/0,
178179
value_fetcher/0,
179180
proxy_object/0,
180-
slimmed_key/0
181+
slimmed_key/0,
182+
head_value/0
181183
]).
182184

183185

@@ -428,6 +430,8 @@ to_querykey(Bucket, Key, Tag, Field, Value) when Tag == ?IDX_TAG ->
428430
-spec to_querykey(key()|null, key()|null, tag()) -> query_key().
429431
%% @doc
430432
%% Convert something into a ledger query key
433+
to_querykey(Bucket, {Key, SubKey}, Tag) ->
434+
{Tag, Bucket, Key, SubKey};
431435
to_querykey(Bucket, Key, Tag) ->
432436
{Tag, Bucket, Key, null}.
433437

@@ -779,19 +783,16 @@ gen_headspec(
779783
gen_headspec({IdxOp, v1, Bucket, Key, SubKey, undefined, Value}, SQN, TTL).
780784

781785

782-
-spec return_proxy
783-
(leveled_head:headonly_tag(), leveled_head:object_metadata(), null, journal_ref())
784-
-> leveled_head:object_metadata();
785-
(leveled_head:object_tag(), leveled_head:object_metadata(), pid(), journal_ref())
786-
-> proxy_objectbin().
786+
-spec return_proxy(
787+
leveled_head:object_tag(),
788+
leveled_head:object_metadata(),
789+
pid(),
790+
journal_ref()) -> proxy_objectbin().
787791
%% @doc
788792
%% If the object has a value, return the metadata and a proxy through which
789-
%% the applictaion or runner can access the value. If it is a ?HEAD_TAG
790-
%% then it has no value, so just return the metadata
791-
return_proxy(?HEAD_TAG, ObjectMetadata, _InkerClone, _JR) ->
792-
% Object has no value - so proxy object makese no sense, just return the
793-
% metadata as is
794-
ObjectMetadata;
793+
%% the application or runner can access the value.
794+
%% This is only called if there is an object tag - i.e. ?RIAK_TAG//STD_TAG or
795+
%% a user-defined tag that uses ObjMetadata in the ?STD_TAG format
795796
return_proxy(Tag, ObjMetadata, InkerClone, JournalRef) ->
796797
Size = leveled_head:get_size(Tag, ObjMetadata),
797798
HeadBin = leveled_head:build_head(Tag, ObjMetadata),
@@ -872,7 +873,7 @@ get_size(PK, Value) ->
872873

873874
-spec get_keyandobjhash(tuple(), tuple()) -> tuple().
874875
%% @doc
875-
%% Return a tucple of {Bucket, Key, Hash} where hash is a hash of the object
876+
%% Return a tuple of {Bucket, Key, Hash} where hash is a hash of the object
876877
%% not the key (for example with Riak tagged objects this will be a hash of
877878
%% the sorted vclock)
878879
get_keyandobjhash(LK, Value) ->

src/leveled_head.erl

+36-37
Original file line numberDiff line numberDiff line change
@@ -49,29 +49,31 @@
4949
-type headonly_tag() :: ?HEAD_TAG.
5050
% Tag assigned to head_only objects. Behaviour cannot be changed
5151

52-
-type riak_metadata() :: {binary()|delete,
53-
% Sibling Metadata
54-
binary()|null,
55-
% Vclock Metadata
56-
non_neg_integer()|null,
57-
% Hash of vclock - non-exportable
58-
non_neg_integer()
59-
% Size in bytes of real object
60-
}.
61-
-type std_metadata() :: {non_neg_integer()|null,
62-
% Hash of value
63-
non_neg_integer(),
64-
% Size in bytes of real object
65-
list(tuple())|undefined
66-
% User-define metadata
67-
}.
68-
-type head_metadata() :: {non_neg_integer()|null,
69-
% Hash of value
70-
non_neg_integer()
71-
% Size in bytes of real object
72-
}.
73-
74-
-type object_metadata() :: riak_metadata()|std_metadata()|head_metadata().
52+
-type riak_metadata() ::
53+
{
54+
binary()|delete,
55+
% Sibling Metadata
56+
binary()|null,
57+
% Vclock Metadata
58+
non_neg_integer()|null,
59+
% Hash of vclock - non-exportable
60+
non_neg_integer()
61+
% Size in bytes of real object
62+
}.
63+
-type std_metadata() ::
64+
{
65+
non_neg_integer()|null,
66+
% Hash of value
67+
non_neg_integer(),
68+
% Size in bytes of real object
69+
list(tuple())|undefined
70+
% User-define metadata
71+
}.
72+
% std_metadata() must be outputted as the metadata format by any
73+
% app-defined function
74+
-type head_metadata() :: leveled_codec:head_value().
75+
76+
-type object_metadata() :: riak_metadata()|std_metadata().
7577

7678
-type appdefinable_function() ::
7779
key_to_canonicalbinary | build_head | extract_metadata | diff_indexspecs.
@@ -80,12 +82,12 @@
8082
-type appdefinable_keyfun() ::
8183
fun((tuple()) -> binary()).
8284
-type appdefinable_headfun() ::
83-
fun((object_tag(), object_metadata()) -> head()).
85+
fun((object_tag(), std_metadata()) -> head()).
8486
-type appdefinable_metadatafun() ::
8587
fun((leveled_codec:tag(), non_neg_integer(), binary()|delete) ->
86-
{object_metadata(), list(erlang:timestamp())}).
88+
{std_metadata(), list(erlang:timestamp())}).
8789
-type appdefinable_indexspecsfun() ::
88-
fun((object_tag(), object_metadata(), object_metadata()|not_present) ->
90+
fun((object_tag(), std_metadata(), std_metadata()|not_present) ->
8991
leveled_codec:index_specs()).
9092
-type appdefinable_function_fun() ::
9193
appdefinable_keyfun() | appdefinable_headfun() |
@@ -96,12 +98,7 @@
9698
-type index_op() :: add | remove.
9799
-type index_value() :: integer() | binary().
98100

99-
-type head() ::
100-
binary()|tuple().
101-
% TODO:
102-
% This is currently not always a binary. Wish is to migrate this so that
103-
% it is predictably a binary
104-
101+
-type head() :: binary()|tuple()|head_metadata().
105102

106103
-export_type([object_tag/0,
107104
headonly_tag/0,
@@ -143,7 +140,9 @@ default_key_to_canonicalbinary(Key) ->
143140
leveled_util:t2b(Key).
144141

145142

146-
-spec build_head(object_tag()|headonly_tag(), object_metadata()) -> head().
143+
-spec build_head
144+
(object_tag(), object_metadata()) -> head();
145+
(headonly_tag(), head_metadata()) -> head() .
147146
%% @doc
148147
%% Return the object metadata as a binary to be the "head" of the object
149148
build_head(?HEAD_TAG, Value) ->
@@ -253,22 +252,22 @@ default_reload_strategy(Tag) ->
253252
{Tag, retain}.
254253

255254
-spec get_size(
256-
object_tag()|headonly_tag(), object_metadata()) -> non_neg_integer().
255+
object_tag(), object_metadata()) -> non_neg_integer().
257256
%% @doc
258257
%% Fetch the size from the metadata
259258
get_size(?RIAK_TAG, {_, _, _, Size}) ->
260259
Size;
261-
get_size(_Tag, {_, Size, _}) ->
260+
get_size(Tag, {_, Size, _}) when Tag =/= ?HEAD_TAG->
262261
Size.
263262

264263

265264
-spec get_hash(
266-
object_tag()|headonly_tag(), object_metadata()) -> non_neg_integer()|null.
265+
object_tag(), object_metadata()) -> non_neg_integer()|null.
267266
%% @doc
268267
%% Fetch the hash from the metadata
269268
get_hash(?RIAK_TAG, {_, _, Hash, _}) ->
270269
Hash;
271-
get_hash(_Tag, {Hash, _, _}) ->
270+
get_hash(Tag, {Hash, _, _}) when Tag =/= ?HEAD_TAG ->
272271
Hash.
273272

274273
-spec standard_hash(any()) -> non_neg_integer().

src/leveled_log.erl

+10-4
Original file line numberDiff line numberDiff line change
@@ -527,13 +527,19 @@ log_wrongkey_test() ->
527527
error,
528528
{badkey, wrong0001},
529529
log(wrong0001, [],[warning, error], ?LOGBASE, backend)
530-
),
530+
).
531+
532+
logtimer_wrongkey_test() ->
533+
ST = os:timestamp(),
534+
% Note -
535+
% An issue with cover means issues with ?assertException, where the
536+
% function being tested is split across lines, the closing bracket on the
537+
% next line is not recognised as being covered. We want 100% coverage, so
538+
% need to write this on one line.
531539
?assertException(
532540
error,
533541
{badkey, wrong0001},
534-
log_timer(
535-
wrong0001, [], os:timestamp(), [warning, error], ?LOGBASE, backend
536-
)
542+
log_timer(wrong0001, [], ST, [warning, error], ?LOGBASE, backend)
537543
).
538544

539545
shouldilog_test() ->

src/leveled_runner.erl

+3-1
Original file line numberDiff line numberDiff line change
@@ -636,7 +636,9 @@ accumulate_objects(FoldObjectsFun, InkerClone, Tag, DeferredFetch) ->
636636
end,
637637
JK = {leveled_codec:to_objectkey(B, K, Tag), SQN},
638638
case DeferredFetch of
639-
{true, JournalCheck} when MD =/= null ->
639+
{true, false} when Tag == ?HEAD_TAG ->
640+
FoldObjectsFun(B, K, MD, Acc);
641+
{true, JournalCheck} when is_tuple(MD) ->
640642
ProxyObj =
641643
leveled_codec:return_proxy(Tag, MD, InkerClone, JK),
642644
case {JournalCheck, InkerClone} of

src/leveled_sst.erl

+1-1
Original file line numberDiff line numberDiff line change
@@ -3377,7 +3377,7 @@ generate_randomkeys(Seqn, Count, Acc, BucketLow, BRange) ->
33773377
Chunk = crypto:strong_rand_bytes(64),
33783378
MV = leveled_codec:convert_to_ledgerv(LK, Seqn, Chunk, 64, infinity),
33793379
MD = element(4, MV),
3380-
MD =/= null orelse error(bad_type),
3380+
is_tuple(MD) orelse error(bad_type),
33813381
?assertMatch(undefined, element(3, MD)),
33823382
MD0 = [{magic_md, [<<0:32/integer>>, base64:encode(Chunk)]}],
33833383
MV0 = setelement(4, MV, setelement(3, MD, MD0)),

test/end_to_end/tictac_SUITE.erl

+71-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
-include("leveled.hrl").
33
-export([all/0, init_per_suite/1, end_per_suite/1]).
44
-export([
5+
multiput_subkeys/1,
56
many_put_compare/1,
67
index_compare/1,
78
basic_headonly/1,
89
tuplebuckets_headonly/1
910
]).
1011

1112
all() -> [
13+
multiput_subkeys,
1214
many_put_compare,
1315
index_compare,
1416
basic_headonly,
@@ -25,8 +27,76 @@ init_per_suite(Config) ->
2527
end_per_suite(Config) ->
2628
testutil:end_per_suite(Config).
2729

28-
many_put_compare(_Config) ->
30+
31+
multiput_subkeys(_Config) ->
32+
multiput_subkeys_byvalue({null, 0}),
33+
multiput_subkeys_byvalue(null),
34+
multiput_subkeys_byvalue(<<"binaryValue">>).
35+
36+
multiput_subkeys_byvalue(V) ->
37+
RootPath = testutil:reset_filestructure("subkeyTest"),
38+
StartOpts = [{root_path, RootPath},
39+
{max_journalsize, 10000000},
40+
{max_pencillercachesize, 12000},
41+
{head_only, no_lookup},
42+
{sync_strategy, testutil:sync_strategy()}],
43+
{ok, Bookie} = leveled_bookie:book_start(StartOpts),
44+
SubKeyCount = 200000,
45+
46+
B = {<<"MultiBucketType">>, <<"MultiBucket">>},
47+
ObjSpecLGen =
48+
fun(K) ->
49+
lists:map(
50+
fun(I) ->
51+
{add, v1, B, K, <<I:32/integer>>, [os:timestamp()], V}
52+
end,
53+
lists:seq(1, SubKeyCount)
54+
)
55+
end,
2956

57+
SpecL1 = ObjSpecLGen(<<1:32/integer>>),
58+
load_objectspecs(SpecL1, 32, Bookie),
59+
SpecL2 = ObjSpecLGen(<<2:32/integer>>),
60+
load_objectspecs(SpecL2, 32, Bookie),
61+
SpecL3 = ObjSpecLGen(<<3:32/integer>>),
62+
load_objectspecs(SpecL3, 32, Bookie),
63+
SpecL4 = ObjSpecLGen(<<4:32/integer>>),
64+
load_objectspecs(SpecL4, 32, Bookie),
65+
SpecL5 = ObjSpecLGen(<<5:32/integer>>),
66+
load_objectspecs(SpecL5, 32, Bookie),
67+
68+
FoldFun =
69+
fun(Bucket, {Key, SubKey}, _Value, Acc) ->
70+
case Bucket of
71+
Bucket when Bucket == B ->
72+
[{Key, SubKey}|Acc]
73+
end
74+
end,
75+
QueryFun =
76+
fun(KeyRange) ->
77+
Range = {range, B, KeyRange},
78+
{async, R} =
79+
leveled_bookie:book_headfold(
80+
Bookie, ?HEAD_TAG, Range, {FoldFun, []}, false, true, false
81+
),
82+
L = length(R()),
83+
io:format("query result for range ~p is ~w~n", [Range, L]),
84+
L
85+
end,
86+
87+
KR1 = {{<<1:32/integer>>, <<>>}, {<<2:32/integer>>, <<>>}},
88+
KR2 = {{<<3:32/integer>>, <<>>}, {<<5:32/integer>>, <<>>}},
89+
KR3 =
90+
{
91+
{<<1:32/integer>>, <<10:32/integer>>},
92+
{<<2:32/integer>>, <<19:32/integer>>}
93+
},
94+
true = SubKeyCount == QueryFun(KR1),
95+
true = (SubKeyCount * 2) == QueryFun(KR2),
96+
true = (SubKeyCount + 10) == QueryFun(KR3),
97+
leveled_bookie:book_destroy(Bookie).
98+
99+
many_put_compare(_Config) ->
30100
TreeSize = small,
31101
SegmentCount = 256 * 256,
32102
% Test requires multiple different databases, so want to mount them all

0 commit comments

Comments
 (0)