Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 328 lines (294 sloc) 12.216 kb
d036073 BossDB is now its own project
Evan Miller authored
1 %% @doc Chicago Boss database abstraction
2
3 -module(boss_db).
4
5 -export([start/1, stop/0]).
6
7 -export([
8 find/1,
9 find/2,
10 find/3,
11 find/4,
12 find/5,
13 find/6,
14 count/1,
15 count/2,
16 counter/1,
17 incr/1,
18 incr/2,
19 delete/1,
20 save_record/1,
21 push/0,
22 pop/0,
23 depth/0,
24 dump/0,
25 execute/1,
26 transaction/1,
27 validate_record/1,
eeb78f0 Validate model parameter types
Evan Miller authored
28 validate_record_types/1,
d036073 BossDB is now its own project
Evan Miller authored
29 type/1,
30 data_type/2]).
31
32 -define(DEFAULT_TIMEOUT, (30 * 1000)).
fd2df1d Manage connections with Poolboy
Evan Miller authored
33 -define(POOLNAME, boss_db_pool).
d036073 BossDB is now its own project
Evan Miller authored
34
35 start(Options) ->
fd2df1d Manage connections with Poolboy
Evan Miller authored
36 AdapterName = proplists:get_value(adapter, Options, mock),
37 Adapter = list_to_atom(lists:concat(["boss_db_adapter_", AdapterName])),
e0c6e90 start/stop for app, init/terminate for connection
Evan Miller authored
38 Adapter:start(Options),
fd2df1d Manage connections with Poolboy
Evan Miller authored
39 lists:foldr(fun(ShardOptions, Acc) ->
40 case proplists:get_value(db_shard_models, ShardOptions, []) of
41 [] -> Acc;
42 _ ->
43 ShardAdapter = case proplists:get_value(db_adapter, ShardOptions) of
44 undefined -> Adapter;
45 ShortName -> list_to_atom(lists:concat(["boss_db_adapter_", ShortName]))
46 end,
e0c6e90 start/stop for app, init/terminate for connection
Evan Miller authored
47 ShardAdapter:start(ShardOptions ++ Options),
fd2df1d Manage connections with Poolboy
Evan Miller authored
48 Acc
49 end
50 end, [], proplists:get_value(shards, Options, [])),
d036073 BossDB is now its own project
Evan Miller authored
51 boss_db_sup:start_link(Options).
52
53 stop() ->
54 ok.
55
a678c25 Fix transactions
Evan Miller authored
56 db_call(Msg) ->
57 case get(boss_db_transaction_info) of
58 undefined ->
59 boss_pool:call(?POOLNAME, Msg, ?DEFAULT_TIMEOUT);
60 State ->
61 {reply, Reply, _} = boss_db_controller:handle_call(Msg, self(), State),
62 Reply
63 end.
64
d036073 BossDB is now its own project
Evan Miller authored
65 %% @spec find(Id::string()) -> BossRecord | {error, Reason}
66 %% @doc Find a BossRecord with the specified `Id'.
67 find("") -> undefined;
68 find(Key) when is_list(Key) ->
a678c25 Fix transactions
Evan Miller authored
69 db_call({find, Key});
d036073 BossDB is now its own project
Evan Miller authored
70 find(_) ->
71 {error, invalid_id}.
72
73 %% @spec find(Type::atom(), Conditions) -> [ BossRecord ]
74 %% @doc Query for BossRecords. Returns all BossRecords of type
75 %% `Type' matching all of the given `Conditions'
76 find(Type, Conditions) ->
77 find(Type, Conditions, all).
78
79 %% @spec find(Type::atom(), Conditions, Max::integer() | all ) -> [ BossRecord ]
80 %% @doc Query for BossRecords. Returns up to `Max' number of BossRecords of type
81 %% `Type' matching all of the given `Conditions'
82 find(Type, Conditions, Max) ->
83 find(Type, Conditions, Max, 0).
84
85 %% @spec find( Type::atom(), Conditions, Max::integer() | all, Skip::integer() ) -> [ BossRecord ]
86 %% @doc Query for BossRecords. Returns up to `Max' number of BossRecords of type
87 %% `Type' matching all of the given `Conditions', skipping the first `Skip' results.
88 find(Type, Conditions, Max, Skip) ->
89 find(Type, Conditions, Max, Skip, id).
90
91 %% @spec find( Type::atom(), Conditions, Max::integer() | all, Skip::integer(), Sort::atom() ) -> [ BossRecord ]
92 %% @doc Query for BossRecords. Returns up to `Max' number of BossRecords of type
93 %% `Type' matching all of the given `Conditions', skipping the
94 %% first `Skip' results, sorted on the attribute `Sort'.
95 find(Type, Conditions, Max, Skip, Sort) ->
96 find(Type, Conditions, Max, Skip, Sort, str_ascending).
97
98 %% @spec find( Type::atom(), Conditions, Max::integer() | all, Skip::integer(), Sort::atom(), SortOrder ) -> [ BossRecord ]
99 %% SortOrder = num_ascending | num_descending | str_ascending | str_descending
100 %% @doc Query for BossRecords. Returns up to `Max' number of BossRecords of type
101 %% Type matching all of the given `Conditions', skipping the
102 %% first `Skip' results, sorted on the attribute `Sort'. `SortOrder' specifies whether
103 %% to treat values as strings or as numbers, and whether to sort ascending or
104 %% descending. (`SortOrder' = `num_ascending', `num_descending', `str_ascending', or
105 %% `str_descending')
106 %%
107 %% Note that Time attributes are stored internally as numbers, so you should
108 %% sort them numerically.
109
110 find(Type, Conditions, Max, Skip, Sort, SortOrder) ->
a678c25 Fix transactions
Evan Miller authored
111 db_call({find, Type, normalize_conditions(Conditions), Max, Skip, Sort, SortOrder}).
d036073 BossDB is now its own project
Evan Miller authored
112
113 %% @spec count( Type::atom() ) -> integer()
114 %% @doc Count the number of BossRecords of type `Type' in the database.
115 count(Type) ->
116 count(Type, []).
117
118 %% @spec count( Type::atom(), Conditions ) -> integer()
119 %% @doc Count the number of BossRecords of type `Type' in the database matching
120 %% all of the given `Conditions'.
121 count(Type, Conditions) ->
a678c25 Fix transactions
Evan Miller authored
122 db_call({count, Type, normalize_conditions(Conditions)}).
d036073 BossDB is now its own project
Evan Miller authored
123
124 %% @spec counter( Id::string() ) -> integer()
125 %% @doc Treat the record associated with `Id' as a counter and return its value.
126 %% Returns 0 if the record does not exist, so to reset a counter just use
127 %% "delete".
128 counter(Key) ->
a678c25 Fix transactions
Evan Miller authored
129 db_call({counter, Key}).
d036073 BossDB is now its own project
Evan Miller authored
130
131 %% @spec incr( Id::string() ) -> integer()
132 %% @doc Treat the record associated with `Id' as a counter and atomically increment its value by 1.
133 incr(Key) ->
134 incr(Key, 1).
135
136 %% @spec incr( Id::string(), Increment::integer() ) -> integer()
137 %% @doc Treat the record associated with `Id' as a counter and atomically increment its value by `Increment'.
138 incr(Key, Count) ->
a678c25 Fix transactions
Evan Miller authored
139 db_call({incr, Key, Count}).
d036073 BossDB is now its own project
Evan Miller authored
140
141 %% @spec delete( Id::string() ) -> ok | {error, Reason}
142 %% @doc Delete the BossRecord with the given `Id'.
143 delete(Key) ->
144 AboutToDelete = boss_db:find(Key),
145 case boss_record_lib:run_before_delete_hooks(AboutToDelete) of
146 ok ->
a678c25 Fix transactions
Evan Miller authored
147 Result = db_call({delete, Key}),
fd2df1d Manage connections with Poolboy
Evan Miller authored
148 case Result of
d036073 BossDB is now its own project
Evan Miller authored
149 ok ->
150 boss_news:deleted(Key, AboutToDelete:attributes()),
151 ok;
fd2df1d Manage connections with Poolboy
Evan Miller authored
152 _ ->
153 Result
d036073 BossDB is now its own project
Evan Miller authored
154 end;
155 {error, Reason} ->
156 {error, Reason}
157 end.
158
159 push() ->
a678c25 Fix transactions
Evan Miller authored
160 db_call(push).
d036073 BossDB is now its own project
Evan Miller authored
161
162 pop() ->
a678c25 Fix transactions
Evan Miller authored
163 db_call(pop).
d036073 BossDB is now its own project
Evan Miller authored
164
165 depth() ->
a678c25 Fix transactions
Evan Miller authored
166 db_call(depth).
d036073 BossDB is now its own project
Evan Miller authored
167
168 dump() ->
a678c25 Fix transactions
Evan Miller authored
169 db_call(dump).
d036073 BossDB is now its own project
Evan Miller authored
170
171 %% @spec execute( Commands::iolist() ) -> RetVal
172 %% @doc Execute raw database commands on SQL databases
173 execute(Commands) ->
a678c25 Fix transactions
Evan Miller authored
174 db_call({execute, Commands}).
d036073 BossDB is now its own project
Evan Miller authored
175
176 %% @spec transaction( TransactionFun::function() ) -> {atomic, Result} | {aborted, Reason}
177 %% @doc Execute a fun inside a transaction.
178 transaction(TransactionFun) ->
a678c25 Fix transactions
Evan Miller authored
179 Worker = poolboy:checkout(?POOLNAME),
180 State = gen_server:call(Worker, state, ?DEFAULT_TIMEOUT),
181 put(boss_db_transaction_info, State),
182 {reply, Reply, _} = boss_db_controller:handle_call({transaction, TransactionFun}, self(), State),
183 put(boss_db_transaction_info, undefined),
184 poolboy:checkin(?POOLNAME, Worker),
185 Reply.
d036073 BossDB is now its own project
Evan Miller authored
186
187 %% @spec save_record( BossRecord ) -> {ok, SavedBossRecord} | {error, [ErrorMessages]}
188 %% @doc Save (that is, create or update) the given BossRecord in the database.
189 %% Performs validation first; see `validate_record/1'.
190 save_record(Record) ->
191 case validate_record(Record) of
192 ok ->
193 RecordId = Record:id(),
194 {IsNew, OldRecord} = if
195 RecordId =:= 'id' ->
196 {true, Record};
197 true ->
198 case find(RecordId) of
199 {error, _Reason} -> {true, Record};
200 undefined -> {true, Record};
201 FoundOldRecord -> {false, FoundOldRecord}
202 end
203 end,
204 HookResult = case boss_record_lib:run_before_hooks(Record, IsNew) of
205 ok -> {ok, Record};
206 {ok, Record1} -> {ok, Record1};
207 {error, Reason} -> {error, Reason}
208 end,
209 case HookResult of
210 {ok, PossiblyModifiedRecord} ->
a678c25 Fix transactions
Evan Miller authored
211 case db_call({save_record, PossiblyModifiedRecord}) of
d036073 BossDB is now its own project
Evan Miller authored
212 {ok, SavedRecord} ->
213 boss_record_lib:run_after_hooks(OldRecord, SavedRecord, IsNew),
214 {ok, SavedRecord};
215 Err -> Err
216 end;
217 Err -> Err
218 end;
219 Err -> Err
220 end.
221
222 %% @spec validate_record( BossRecord ) -> ok | {error, [ErrorMessages]}
223 %% @doc Validate the given BossRecord without saving it in the database.
224 %% `ErrorMessages' are generated from the list of tests returned by the BossRecord's
225 %% `validation_tests/0' function (if defined). The returned list should consist of
226 %% `{TestFunction, ErrorMessage}' tuples, where `TestFunction' is a fun of arity 0
227 %% that returns `true' if the record is valid or `false' if it is invalid.
228 %% `ErrorMessage' should be a (constant) string which will be included in `ErrorMessages'
229 %% if the `TestFunction' returns `false' on this particular BossRecord.
230 validate_record(Record) ->
231 Type = element(1, Record),
eeb78f0 Validate model parameter types
Evan Miller authored
232 Errors1 = case validate_record_types(Record) of
233 ok -> [];
234 {error, Errors} -> Errors
d036073 BossDB is now its own project
Evan Miller authored
235 end,
eeb78f0 Validate model parameter types
Evan Miller authored
236 Errors2 = case Errors1 of
237 [] ->
238 case erlang:function_exported(Type, validation_tests, 1) of
239 true -> [String || {TestFun, String} <- Record:validation_tests(), not TestFun()];
240 false -> []
241 end;
242 _ -> Errors1
243 end,
244 case length(Errors2) of
d036073 BossDB is now its own project
Evan Miller authored
245 0 -> ok;
eeb78f0 Validate model parameter types
Evan Miller authored
246 _ -> {error, Errors2}
247 end.
248
249 %% @spec validate_record_types( BossRecord ) -> ok | {error, [ErrorMessages]}
250 %% @doc Validate the parameter types of the given BossRecord without saving it
251 %% to the database.
252 validate_record_types(Record) ->
253 Errors = lists:foldl(fun
254 ({Attr, Type}, Acc) ->
255 Data = Record:Attr(),
256 GreatSuccess = case {Data, Type} of
d44c938 Cast attribute values to specified types
Evan Miller authored
257 {undefined, _} ->
258 true;
eeb78f0 Validate model parameter types
Evan Miller authored
259 {Data, string} when is_list(Data) ->
260 true;
261 {Data, binary} when is_binary(Data) ->
262 true;
263 {{{D1, D2, D3}, {T1, T2, T3}}, datetime} when is_integer(D1), is_integer(D2), is_integer(D3),
264 is_integer(T1), is_integer(T2), is_integer(T3) ->
265 true;
266 {Data, integer} when is_integer(Data) ->
267 true;
268 {Data, float} when is_float(Data) ->
269 true;
d44c938 Cast attribute values to specified types
Evan Miller authored
270 {Data, boolean} when is_boolean(Data) ->
eeb78f0 Validate model parameter types
Evan Miller authored
271 true;
d44c938 Cast attribute values to specified types
Evan Miller authored
272 {{N1, N2, N3}, timestamp} when is_integer(N1), is_integer(N2), is_integer(N3) ->
eeb78f0 Validate model parameter types
Evan Miller authored
273 true;
274 {_Data, Type} ->
275 false
276 end,
277 if
278 GreatSuccess ->
279 Acc;
280 true ->
281 [lists:concat(["Invalid data type for ", Attr])|Acc]
282 end
283 end, [], Record:attribute_types()),
284 case Errors of
285 [] -> ok;
d036073 BossDB is now its own project
Evan Miller authored
286 _ -> {error, Errors}
287 end.
288
289 %% @spec type( Id::string() ) -> Type::atom()
290 %% @doc Returns the type of the BossRecord with `Id', or `undefined' if the record does not exist.
291 type(Key) ->
292 case find(Key) of
293 undefined -> undefined;
294 Record -> element(1, Record)
295 end.
296
297 data_type(_, _Val) when is_float(_Val) ->
298 "float";
299 data_type(_, _Val) when is_binary(_Val) ->
300 "binary";
301 data_type(_, _Val) when is_integer(_Val) ->
302 "integer";
303 data_type(_, _Val) when is_tuple(_Val) ->
304 "datetime";
305 data_type(_, _Val) when is_boolean(_Val) ->
306 "boolean";
307 data_type(_, undefined) ->
308 "null";
309 data_type('id', _) ->
310 "id";
311 data_type(Key, Val) when is_list(Val) ->
312 case lists:suffix("_id", atom_to_list(Key)) of
313 true -> "foreign_id";
314 false -> "string"
315 end.
316
317 normalize_conditions(Conditions) ->
318 normalize_conditions(Conditions, []).
319
320 normalize_conditions([], Acc) ->
321 lists:reverse(Acc);
322 normalize_conditions([Key, Operator, Value|Rest], Acc) when is_atom(Key), is_atom(Operator) ->
323 normalize_conditions(Rest, [{Key, Operator, Value}|Acc]);
324 normalize_conditions([{Key, Value}|Rest], Acc) when is_atom(Key) ->
325 normalize_conditions(Rest, [{Key, 'equals', Value}|Acc]);
326 normalize_conditions([{Key, Operator, Value}|Rest], Acc) when is_atom(Key), is_atom(Operator) ->
327 normalize_conditions(Rest, [{Key, Operator, Value}|Acc]).
Something went wrong with that request. Please try again.