diff --git a/src/boss_db.erl b/src/boss_db.erl index f13f9083..f80f53a9 100644 --- a/src/boss_db.erl +++ b/src/boss_db.erl @@ -5,6 +5,8 @@ -export([start/1, stop/0]). -export([ + paginate/3, + paginate/4, migrate/1, migrate/2, find/1, @@ -140,6 +142,17 @@ migrate({Tag, Fun}, Direction) -> Fun(Direction), db_call({migration_done, Tag, Direction}). + +%% @spec paginate(Model::atom(), Conditions, Opts::proplists()) -> {Page::integer(), TotalPage::integer(), Total::integer(), Result::list()] | {error, Reason} +%% @doc Paginate through the results matching the conditions. Use `Opts' [{page, +%% PageNum}, {page_size, PageSize}] to control which page to fetch, +%% and how many results per page. Page size defaults to 10. +paginate(Type, Conditions, Opts) -> + paginate(Type, Conditions, Opts, ?DEFAULT_TIMEOUT). + +paginate(Type, Conditions, Opts, Timeout) -> + db_call({paginate, Type, Conditions, Opts}, Timeout). + %% @spec find(Id::string()) -> Value | {error, Reason} %% @doc Find a BossRecord with the specified `Id' (e.g. "employee-42") or a value described %% by a dot-separated path (e.g. "employee-42.manager.name"). diff --git a/src/boss_db_adapter.erl b/src/boss_db_adapter.erl index c193d970..8e28fe6d 100644 --- a/src/boss_db_adapter.erl +++ b/src/boss_db_adapter.erl @@ -6,7 +6,8 @@ behaviour_info(callbacks) -> [ {start, 1}, {stop, 0}, {init, 1}, {terminate, 1}, {find, 2}, {find, 7}, {count, 3}, - {delete, 2}, {counter, 2}, {incr, 3}, {save_record, 2} + {delete, 2}, {counter, 2}, {incr, 3}, {save_record, 2}, + {paginate, 4} ]; behaviour_info(_Other) -> undefined. diff --git a/src/boss_db_controller.erl b/src/boss_db_controller.erl index e3e3b576..00fdd864 100644 --- a/src/boss_db_controller.erl +++ b/src/boss_db_controller.erl @@ -99,6 +99,9 @@ init(Options) -> handle_call(_Anything, _Anyone, State) when State#state.connection_state /= connected -> {reply, db_connection_down, State}; +handle_call({paginate, Type, Conditions, Opts}, _From, #state{ cache_enable = false } = State) -> + {Adapter, Conn, _} = db_for_type(Type, State), + {reply, Adapter:paginate(Conn, Type, Conditions, Opts), State}; handle_call({find, Key}, From, #state{ cache_enable = true, cache_prefix = Prefix } = State) -> CacheResult = boss_cache:get(Prefix, Key), find_by_key(Key, From, Prefix, State, CacheResult); diff --git a/src/db_adapters/boss_db_adapter_mnesia.erl b/src/db_adapters/boss_db_adapter_mnesia.erl index 131c3a44..92d773d0 100644 --- a/src/db_adapters/boss_db_adapter_mnesia.erl +++ b/src/db_adapters/boss_db_adapter_mnesia.erl @@ -4,7 +4,9 @@ -export([count/3, counter/2, incr/3, delete/2, save_record/2]). -export([transaction/2]). -export([table_exists/2, get_migrations_table/1, migration_done/3]). +-export([paginate/4]). +-define(DEFAULT_PAGE_SIZE, 10). %-define(TRILLION, (1000 * 1000 * 1000 * 1000)). start(_) -> @@ -22,6 +24,25 @@ init(_Options) -> terminate(_) -> ok. +paginate(_, Model, Conditions, Opts) -> + {Pattern, _Filter} = build_query(Model, Conditions), %% don't know if _Filter is usefull here ?? + Page = proplists:get_value(page, Opts, 1), + PageSize = proplists:get_value(page_size, Opts, ?DEFAULT_PAGE_SIZE), + Offset = PageSize * (Page - 1), + Total = boss_db:count(Model, Conditions), + TotalPages = (Total div PageSize) + (case Total rem PageSize of + 0 -> 0; + _ -> 1 + end), + MatchSpec = [{list_to_tuple([Model|Pattern]), [], ['$_']}], + case limit(Model, Offset, PageSize, MatchSpec) of + {atomic, Result} -> + {Page, TotalPages, Total, Result}; + + {aborted, Reason} -> + {error, Reason} + end. + % ----- find(_, Id) when is_list(Id) -> Type = infer_type_from_id(Id), @@ -157,8 +178,18 @@ test_rec(Rec,{Key, 'contains_none', Values}) when is_list(Values) -> lists:any(fun (Ele) -> not lists:member(Ele, apply(Rec,Key,[])) end, Values). % ----- -count(Conn, Type, Conditions) -> - length(find(Conn, Type, Conditions, all, 0, id, ascending)). + +count(_, Type, Conditions) -> + {Pattern, _} = build_query(Type, Conditions), + MatchSpec = [{list_to_tuple([Type|Pattern]), [], [1]}], + FunCount = fun() -> mnesia:select(Type, MatchSpec) end, + case mnesia:transaction(FunCount) of + {atomic, Result} -> lists:sum(Result); + {aborted, Reason} -> {error, Reason} + end. + +%% count(Conn, Type, Conditions) -> +%% length(find(Conn, Type, Conditions, all, 0, id, ascending)). % ----- counter(Conn, Id) when is_list(Id) -> @@ -254,6 +285,8 @@ infer_type_from_id(Id) when is_list(Id) -> list_to_atom(hd(string:tokens(Id, "-"))). %----- +build_query(Type, Conditions) -> + build_query(Type, Conditions, none, none, none, none). build_query(Type, Conditions, _Max, _Skip, _Sort, _SortOrder) -> % a Query is a {Pattern, Filter} combo Fldnames = mnesia:table_info(Type, attributes), BlankPattern = [ {Fld, '_'} || Fld <- Fldnames], @@ -273,3 +306,44 @@ build_conditions1([First|Rest], Pattern, Filter) -> build_conditions1(Rest, Pattern, [First|Filter]). +limit (Tab, Offset, Number, MatchSpec) -> + Fun = fun() -> + seek (Offset, + Number, + mnesia:select (Tab, + MatchSpec, + Number, + read) + ) + end, + mnesia:transaction(Fun). + + +seek (_Offset, _Number, '$end_of_table') -> + []; +seek (Offset, Number, X) when Offset =< 0 -> + read (Number, X, []); +seek (Offset, Number, { Results, Cont }) -> + NumResults = length (Results), + case Offset > NumResults of + true -> + seek (Offset - NumResults, Number, mnesia:select (Cont)); + false -> + { _, DontDrop } = lists:split (Offset, Results), + Keep = lists:sublist (DontDrop, Number), + read (Number - length (Keep), mnesia:select (Cont), [ Keep ]) + end. + +read (Number, _, Acc) when Number =< 0 -> + lists:foldl (fun erlang:'++'/2, [], Acc); +read (_Number, '$end_of_table', Acc) -> + lists:foldl (fun erlang:'++'/2, [], Acc); +read (Number, { Results, Cont }, Acc) -> + NumResults = length (Results), + case Number > NumResults of + true -> + read (Number - NumResults, mnesia:select (Cont), [ Results | Acc ]); + false -> + { Keep, _ } = lists:split (Number, Results), + lists:foldl (fun erlang:'++'/2, Keep, Acc) + end.