Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion include/yokozuna.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@
-type ring_event() :: {ring_event, riak_core_ring:riak_core_ring()}.
-type event() :: ring_event().

%% @doc
-type schema_err() :: {error, string()}.

%% @doc The `component()' type represents components that may be
%% enabled or disabled at runtime. Typically a component is
%% disabled in a live, production cluster in order to isolate
Expand All @@ -105,7 +108,6 @@
%% action to manually index the missing data or wait for AAE to
%% take care of it.
-type component() :: search | index.

%%%===================================================================
%%% Macros
%%%===================================================================
Expand Down
183 changes: 181 additions & 2 deletions src/yz_console.erl
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,24 @@
-module(yz_console).
-include("yokozuna.hrl").
-export([aae_status/1,
switch_to_new_search/1]).
switch_to_new_search/1,
create_schema/1,
show_schema/1,
add_to_schema/1,
remove_from_schema/1]).

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.

-type field_data() :: {dynamicfield | field, list()}.

-define(LIST_TO_ATOM(L), list_to_atom(L)).
-define(LIST_TO_BINARY(L), list_to_binary(L)).
-define(FIELD_DEFAULTS, [{type, "text_general"},
{indexed, "true"},
{stored, "false"},
{multiValued, "true"}]).

%% @doc Print the Active Anti-Entropy status to stdout.
-spec aae_status([]) -> ok.
Expand All @@ -41,7 +58,8 @@ aae_status([]) ->
%% back without restarting the cluster.
-spec switch_to_new_search([]) -> ok | {error, {nodes_down, [node()]}}.
switch_to_new_search([]) ->
{_Good, Down} = riak_core_util:rpc_every_member_ann(yokozuna, switch_to_yokozuna, [], 5000),
{_Good, Down} = riak_core_util:rpc_every_member_ann(
yokozuna, switch_to_yokozuna, [], 5000),
case Down of
[] ->
ok;
Expand All @@ -51,3 +69,164 @@ switch_to_new_search([]) ->
io:format(standard_error, "The following nodes could not be reached: ~s", [DownStr]),
{error, {nodes_down, Down}}
end.

%% @doc Creates (and overrides) schema for name and file path.
%% riak-admin search schema create <schema name> <path to file>
-spec create_schema([string()|string()]) -> ok | error.
create_schema([Name, Path]) ->
try
{ok, RawSchema} = read_schema(Path),
FMTName = ?LIST_TO_ATOM(Name),
case yz_schema:store(?LIST_TO_BINARY(Name), RawSchema) of
ok ->
io:format("~p schema created~n", [FMTName]),
ok;
{error, _} ->
io:format("Error creating schema ~p~n", [FMTName]),
error
end
catch {fileReadError, _} ->
error
end.

%% @doc Shows solr schema for name passed in.
%% riak-admin search schema show <name of schema>
-spec show_schema([string()]) -> ok | error.
show_schema([Name]) ->
FMTName = ?LIST_TO_ATOM(Name),
case yz_schema:get(?LIST_TO_BINARY(Name)) of
{ok, R} ->
io:format("Schema ~p:~n~s~n", [FMTName, binary_to_list(R)]),
ok;
{error, notfound} ->
io:format("Schema ~p not found~n", [FMTName]),
error
end.

%% @doc Adds field of <fieldname> to schema contents.
%% riak-admin search schema <name> add field|dynamicfield <fieldname> [<option>=<value> [...]]
-spec add_to_schema([string()|string()]) -> ok | error.
add_to_schema([Name,FieldType,FieldName|Options]) ->
try
ParsedOptions = [{?LIST_TO_ATOM(K), V} || {K, V} <- parse_options(Options)],
Field = make_field(?LIST_TO_ATOM(FieldType), FieldName, ParsedOptions),
%% TODO: Wrap *update_schema* in a Case to check for ok|error
update_schema(add, Name, Field),
ok
catch {error, {invalid_option, Option}} ->
io:format("Invalid Option: ~p~n", [?LIST_TO_ATOM(Option)]),
error
end.


%% @doc Removes field of <fieldname> from schema contents.
%% riak-admin search schema <name> remove <fieldname>
-spec remove_from_schema([string()|string()]) -> ok | error.
remove_from_schema([Name, FieldName]) ->
%% TODO: Wrap Update in a Case to check for ok|error
update_schema(remove, Name, FieldName),
ok.

%%%===================================================================
%%% Private
%%%===================================================================

%% @doc Create field tagged tuple.
-spec make_field(atom(), string(), list()) -> field_data().
make_field(dynamicfield, FieldName, Options) ->
{dynamicfield, [{name, FieldName}|merge(Options, ?FIELD_DEFAULTS)]};
make_field(field, FieldName, Options) ->
{field, [{name, FieldName}|merge(Options, ?FIELD_DEFAULTS)]}.

%% @doc Update schema with change(s).
-spec update_schema(add | remove, string(), field_data() | string()) ->
ok | schema_err().
update_schema(add, Name, Field) ->
{Name, Field};
update_schema(remove, Name, FieldName) ->
{Name, FieldName}.

%% TODO: Use Riak-Cli or place *parse_options* in a one place for all consoles
-spec parse_options(list(string())) -> list({string(), string()}).
parse_options(Options) ->
parse_options(Options, []).

parse_options([], Acc) ->
Acc;
parse_options([H|T], Acc) ->
case re:split(H, "=", [{parts, 2}, {return, list}]) of
[Key, Value] when is_list(Key), is_list(Value) ->
parse_options(T, [{string:to_lower(Key), Value}|Acc]);
_Other ->
throw({error, {invalid_option, H}})
end.

%% @doc Reads and returns `RawSchema` from file path.
-spec read_schema(string()) -> {ok, raw_schema()} | schema_err().
read_schema(Path) ->
AbsPath = filename:absname(Path),
case file:read_file(AbsPath) of
{ok, RawSchema} ->
{ok, RawSchema};
{error, enoent} ->
io:format("No such file or directory: ~s~n", [Path]),
throw({fileReadError, enoent});
{error, Reason} ->
?ERROR("Error reading file ~s:~p", [Path, Reason]),
io:format("Error reading file ~s, see log for details~n", [Path]),
throw({fileReadError, Reason})
end.

%% @doc Merge field defaults.
-spec merge([{atom(), any()}], [{atom(), any()}]) -> [{atom(), any()}].
merge(Overriding, Other) ->
lists:ukeymerge(1, lists:ukeysort(1, Overriding),
lists:ukeysort(1, Other)).

%% ===================================================================
%% EUnit tests
%% ===================================================================

-ifdef(TEST).

read_schema_test() ->
{ok, CurrentDir} = file:get_cwd(),
MissingSchema = CurrentDir ++ "/foo.xml",
GoodOutput = read_schema("../priv/default_schema.xml"),
?assertMatch({ok, _}, GoodOutput),
{ok, RawSchema} = GoodOutput,
?assert(is_binary(RawSchema)),
?assertThrow({fileReadError, enoent}, read_schema(MissingSchema)).

parse_options_test() ->
EmptyOptions = [],
GoodOptions = ["foo=bar", "bar=baz", "foobar=barbaz"],
BadOptions = ["hey", "foo"],
?assertEqual([], parse_options(EmptyOptions)),
%% first errored option will be thrown
?assertThrow({error, {invalid_option, "hey"}}, parse_options(BadOptions)),
?assertEqual([{"foobar", "barbaz"}, {"bar", "baz"}, {"foo", "bar"}],
parse_options(GoodOptions)).

merge_test() ->
Overriding = [{type, "integer"}, {stored, "true"}, {indexed, "true"}],
Other = ?FIELD_DEFAULTS,
Merged = merge(Overriding, Other),
Expected = [{indexed, "true"}, {multiValued, "true"}, {stored, "true"},
{type, "integer"}],
?assertEqual(Expected, Merged).

make_field_test() ->
Field = make_field(field, "person",
parse_options(["foo=bar", "bar=baz"])),
DynamicField = make_field(dynamicfield, "person", []),
?assertMatch({field, [_|_]}, Field),
?assertMatch({dynamicfield, [_|_]}, DynamicField),
{_, FieldItems} = Field,
{_, DynamicFieldItems} = DynamicField,
%% + 2 new options + 1 for the field name
?assertEqual(length(FieldItems), length(?FIELD_DEFAULTS) + 3),
%% + 1 for the field name
?assertEqual(length(DynamicFieldItems), length(?FIELD_DEFAULTS) + 1).

-endif.
1 change: 0 additions & 1 deletion src/yz_schema.erl
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
-compile(export_all).

-define(SCHEMA_VSN, "1.5").
-type schema_err() :: {error, string()}.

%%%===================================================================
%%% API
Expand Down