% Licensed 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(mango_idx_nouveau). -export([ validate_new/2, validate_fields/1, validate_index_def/1, add/2, remove/2, from_ddoc/1, to_json/1, columns/1, is_usable/3, get_default_field_options/1 ]). -include_lib("couch/include/couch_db.hrl"). -include("mango.hrl"). -include("mango_idx.hrl"). validate_new(#idx{} = Idx, Db) -> {ok, Def} = do_validate(Idx#idx.def), maybe_reject_index_all_req(Def, Db), {ok, Idx#idx{def = Def}}. validate_index_def(IndexInfo) -> do_validate(IndexInfo). add(#doc{body = {Props0}} = DDoc, Idx) -> Texts1 = case proplists:get_value(<<"nouveau">>, Props0) of {Texts0} -> Texts0; _ -> [] end, NewText = make_text(Idx), Texts2 = lists:keystore(element(1, NewText), 1, Texts1, NewText), Props1 = lists:keystore(<<"nouveau">>, 1, Props0, {<<"nouveau">>, {Texts2}}), {ok, DDoc#doc{body = {Props1}}}. remove(#doc{body = {Props0}} = DDoc, Idx) -> Texts1 = case proplists:get_value(<<"nouveau">>, Props0) of {Texts0} -> Texts0; _ -> ?MANGO_ERROR({index_not_found, Idx#idx.name}) end, Texts2 = lists:keydelete(Idx#idx.name, 1, Texts1), if Texts2 /= Texts1 -> ok; true -> ?MANGO_ERROR({index_not_found, Idx#idx.name}) end, Props1 = case Texts2 of [] -> lists:keydelete(<<"nouveau">>, 1, Props0); _ -> lists:keystore(<<"nouveau">>, 1, Props0, {<<"nouveau">>, {Texts2}}) end, {ok, DDoc#doc{body = {Props1}}}. from_ddoc({Props}) -> case lists:keyfind(<<"nouveau">>, 1, Props) of {<<"nouveau">>, {Texts}} when is_list(Texts) -> lists:flatmap( fun({Name, {VProps}}) -> case validate_ddoc(VProps) of invalid_ddoc -> []; Def -> I = #idx{ type = <<"nouveau">>, name = Name, def = Def }, [I] end end, Texts ); _ -> [] end. to_json(Idx) -> {[ {ddoc, Idx#idx.ddoc}, {name, Idx#idx.name}, {type, Idx#idx.type}, {partitioned, Idx#idx.partitioned}, {def, {def_to_json(Idx#idx.def)}} ]}. columns(Idx) -> {Props} = Idx#idx.def, {<<"fields">>, Fields} = lists:keyfind(<<"fields">>, 1, Props), case Fields of <<"all_fields">> -> all_fields; _ -> {DFProps} = couch_util:get_value(<<"default_field">>, Props, {[]}), Enabled = couch_util:get_value(<<"enabled">>, DFProps, true), Default = case Enabled of true -> [<<"$default">>]; false -> [] end, Default ++ lists:map( fun({FProps}) -> {_, Name} = lists:keyfind(<<"name">>, 1, FProps), {_, Type} = lists:keyfind(<<"type">>, 1, FProps), iolist_to_binary([Name, ":", Type]) end, Fields ) end. is_usable(_, Selector, _) when Selector =:= {[]} -> false; is_usable(Idx, Selector, _) -> case columns(Idx) of all_fields -> true; Cols -> Fields = indexable_fields(Selector), sets:is_subset(sets:from_list(Fields), sets:from_list(Cols)) end. do_validate({Props}) -> {ok, Opts} = mango_opts:validate(Props, opts()), {ok, {Opts}}; do_validate(Else) -> ?MANGO_ERROR({invalid_index_text, Else}). def_to_json({Props}) -> def_to_json(Props); def_to_json([]) -> []; def_to_json([{<<"fields">>, <<"all_fields">>} | Rest]) -> [{<<"fields">>, []} | def_to_json(Rest)]; def_to_json([{fields, Fields} | Rest]) -> [{<<"fields">>, fields_to_json(Fields)} | def_to_json(Rest)]; def_to_json([{<<"fields">>, Fields} | Rest]) -> [{<<"fields">>, fields_to_json(Fields)} | def_to_json(Rest)]; % Don't include partial_filter_selector in the json conversion % if its the default value def_to_json([{<<"partial_filter_selector">>, {[]}} | Rest]) -> def_to_json(Rest); def_to_json([{Key, Value} | Rest]) -> [{Key, Value} | def_to_json(Rest)]. fields_to_json([]) -> []; fields_to_json([{[{<<"name">>, Name}, {<<"type">>, Type0}]} | Rest]) -> ok = validate_field_name(Name), Type = validate_field_type(Type0), [{[{Name, Type}]} | fields_to_json(Rest)]; fields_to_json([{[{<<"type">>, Type0}, {<<"name">>, Name}]} | Rest]) -> ok = validate_field_name(Name), Type = validate_field_type(Type0), [{[{Name, Type}]} | fields_to_json(Rest)]. %% In the future, we can possibly add more restrictive validation. %% For now, let's make sure the field name is not blank. validate_field_name(<<"">>) -> throw(invalid_field_name); validate_field_name(Else) when is_binary(Else) -> ok; validate_field_name(_) -> throw(invalid_field_name). validate_field_type(<<"string">>) -> <<"string">>; validate_field_type(<<"text">>) -> <<"text">>; validate_field_type(<<"number">>) -> <<"number">>; validate_field_type(<<"boolean">>) -> <<"boolean">>. validate_fields(<<"all_fields">>) -> {ok, all_fields}; validate_fields(Fields) -> try fields_to_json(Fields) of _ -> mango_fields:new(Fields) catch error:function_clause -> ?MANGO_ERROR({invalid_index_fields_definition, Fields}); throw:invalid_field_name -> ?MANGO_ERROR({invalid_index_fields_definition, Fields}) end. validate_ddoc(VProps) -> try Def = proplists:get_value(<<"index">>, VProps), validate_index_def(Def), Def catch Error:Reason -> couch_log:error( "Invalid Index Def ~p: Error. ~p, Reason: ~p", [VProps, Error, Reason] ), invalid_ddoc end. opts() -> [ {<<"default_analyzer">>, [ {tag, default_analyzer}, {optional, true}, {default, <<"keyword">>} ]}, {<<"default_field">>, [ {tag, default_field}, {optional, true}, {default, {[]}} ]}, {<<"partial_filter_selector">>, [ {tag, partial_filter_selector}, {optional, true}, {default, {[]}}, {validator, fun mango_opts:validate_selector/1} ]}, {<<"selector">>, [ {tag, selector}, {optional, true}, {default, {[]}}, {validator, fun mango_opts:validate_selector/1} ]}, {<<"fields">>, [ {tag, fields}, {optional, true}, {default, []}, {validator, fun ?MODULE:validate_fields/1} ]}, {<<"index_array_lengths">>, [ {tag, index_array_lengths}, {optional, true}, {default, true}, {validator, fun mango_opts:is_boolean/1} ]} ]. make_text(Idx) -> Text = {[ {<<"index">>, Idx#idx.def}, {<<"analyzer">>, construct_analyzer(Idx#idx.def)} ]}, {Idx#idx.name, Text}. get_default_field_options(Props) -> Default = couch_util:get_value(default_field, Props, {[]}), case Default of Bool when is_boolean(Bool) -> {Bool, <<"standard">>}; {[]} -> {true, <<"standard">>}; {Opts} -> Enabled = couch_util:get_value(<<"enabled">>, Opts, true), Analyzer = couch_util:get_value( <<"analyzer">>, Opts, <<"standard">> ), {Enabled, Analyzer} end. construct_analyzer({Props}) -> DefaultAnalyzer = couch_util:get_value( default_analyzer, Props, <<"keyword">> ), {DefaultField, DefaultFieldAnalyzer} = get_default_field_options(Props), DefaultAnalyzerDef = case DefaultField of true -> [{<<"$default">>, DefaultFieldAnalyzer}]; _ -> [] end, case DefaultAnalyzerDef of [] -> <<"keyword">>; _ -> {[ {<<"name">>, <<"perfield">>}, {<<"default">>, DefaultAnalyzer}, {<<"fields">>, {DefaultAnalyzerDef}} ]} end. indexable_fields(Selector) -> TupleTree = mango_selector_text:convert([], Selector), indexable_fields([], TupleTree). indexable_fields(Fields, {op_and, Args}) when is_list(Args) -> lists:foldl( fun(Arg, Fields0) -> indexable_fields(Fields0, Arg) end, Fields, Args ); %% For queries that use array element access or $in operations, two %% fields get generated by mango_selector_text:convert. At index %% definition time, only one field gets defined. In this situation, we %% remove the extra generated field so that the index can be used. For %% all other situations, we include the fields as normal. indexable_fields( Fields, {op_or, [ {op_field, Field0}, {op_field, {[Name | _], _}} = Field1 ]} ) -> case lists:member(<<"[]">>, Name) of true -> indexable_fields(Fields, {op_field, Field0}); false -> Fields1 = indexable_fields(Fields, {op_field, Field0}), indexable_fields(Fields1, Field1) end; indexable_fields(Fields, {op_or, Args}) when is_list(Args) -> lists:foldl( fun(Arg, Fields0) -> indexable_fields(Fields0, Arg) end, Fields, Args ); indexable_fields(Fields, {op_not, {ExistsQuery, Arg}}) when is_tuple(Arg) -> Fields0 = indexable_fields(Fields, ExistsQuery), indexable_fields(Fields0, Arg); % forces "$exists" : false to use _all_docs indexable_fields(_, {op_not, {_, false}}) -> []; indexable_fields(Fields, {op_insert, Arg}) when is_binary(Arg) -> Fields; %% fieldname.[]:length is not a user defined field. indexable_fields(Fields, {op_field, {[_, <<":length">>], _}}) -> Fields; indexable_fields(Fields, {op_field, {Name, _}}) -> [iolist_to_binary(Name) | Fields]; %% In this particular case, the lucene index is doing a field_exists query %% so it is looking at all sorts of combinations of field:* and field.* %% We don't add the field because we cannot pre-determine what field will exist. %% Hence we just return Fields and make it less restrictive. indexable_fields(Fields, {op_fieldname, {_, _}}) -> Fields; %% Similar idea to op_fieldname but with fieldname:null indexable_fields(Fields, {op_null, {_, _}}) -> Fields; indexable_fields(Fields, {op_default, _}) -> [<<"$default">> | Fields]. maybe_reject_index_all_req({Def}, Db) -> DbName = couch_db:name(Db), #user_ctx{name = User} = couch_db:get_user_ctx(Db), Fields = couch_util:get_value(fields, Def), case {Fields, forbid_index_all()} of {all_fields, "true"} -> ?MANGO_ERROR(index_all_disabled); {all_fields, "warn"} -> couch_log:warning( "User ~p is indexing all fields in db ~p", [User, DbName] ); _ -> ok end. forbid_index_all() -> config:get("mango", "index_all_disabled", "false"). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). setup_all() -> Ctx = test_util:start_couch(), meck:expect( couch_log, warning, 2, fun(_, _) -> throw({test_error, logged_warning}) end ), Ctx. teardown_all(Ctx) -> meck:unload(), test_util:stop_couch(Ctx). setup() -> %default index all def that generates {fields, all_fields} Index = #idx{def = {[]}}, DbName = <<"testdb">>, UserCtx = #user_ctx{name = <<"u1">>}, {ok, Db} = couch_db:clustered_db(DbName, UserCtx), {Index, Db}. teardown(_) -> ok. index_all_test_() -> { setup, fun setup_all/0, fun teardown_all/1, { foreach, fun setup/0, fun teardown/1, [ fun forbid_index_all/1, fun default_and_false_index_all/1, fun warn_index_all/1 ] } }. forbid_index_all({Idx, Db}) -> ?_test(begin ok = config:set("mango", "index_all_disabled", "true", false), ?assertThrow( {mango_error, ?MODULE, index_all_disabled}, validate_new(Idx, Db) ) end). default_and_false_index_all({Idx, Db}) -> ?_test(begin config:delete("mango", "index_all_disabled", false), {ok, #idx{def = {Def}}} = validate_new(Idx, Db), Fields = couch_util:get_value(fields, Def), ?assertEqual(all_fields, Fields), ok = config:set("mango", "index_all_disabled", "false", false), {ok, #idx{def = {Def2}}} = validate_new(Idx, Db), Fields2 = couch_util:get_value(fields, Def2), ?assertEqual(all_fields, Fields2) end). warn_index_all({Idx, Db}) -> ?_test(begin ok = config:set("mango", "index_all_disabled", "warn", false), ?assertThrow({test_error, logged_warning}, validate_new(Idx, Db)) end). -endif.