Skip to content
Newer
Older
100644 332 lines (298 sloc) 12.1 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) ->
6eb2514 boss_db:delete/1 crashed on non-existent key
Evan Miller authored
144 case boss_db:find(Key) of
145 undefined ->
146 {error, not_found};
147 AboutToDelete ->
148 case boss_record_lib:run_before_delete_hooks(AboutToDelete) of
149 ok ->
150 Result = db_call({delete, Key}),
151 case Result of
152 ok ->
153 boss_news:deleted(Key, AboutToDelete:attributes()),
154 ok;
155 _ ->
156 Result
157 end;
158 {error, Reason} ->
159 {error, Reason}
160 end
d036073 BossDB is now its own project
Evan Miller authored
161 end.
162
163 push() ->
a678c25 Fix transactions
Evan Miller authored
164 db_call(push).
d036073 BossDB is now its own project
Evan Miller authored
165
166 pop() ->
a678c25 Fix transactions
Evan Miller authored
167 db_call(pop).
d036073 BossDB is now its own project
Evan Miller authored
168
169 depth() ->
a678c25 Fix transactions
Evan Miller authored
170 db_call(depth).
d036073 BossDB is now its own project
Evan Miller authored
171
172 dump() ->
a678c25 Fix transactions
Evan Miller authored
173 db_call(dump).
d036073 BossDB is now its own project
Evan Miller authored
174
175 %% @spec execute( Commands::iolist() ) -> RetVal
176 %% @doc Execute raw database commands on SQL databases
177 execute(Commands) ->
a678c25 Fix transactions
Evan Miller authored
178 db_call({execute, Commands}).
d036073 BossDB is now its own project
Evan Miller authored
179
180 %% @spec transaction( TransactionFun::function() ) -> {atomic, Result} | {aborted, Reason}
181 %% @doc Execute a fun inside a transaction.
182 transaction(TransactionFun) ->
a678c25 Fix transactions
Evan Miller authored
183 Worker = poolboy:checkout(?POOLNAME),
184 State = gen_server:call(Worker, state, ?DEFAULT_TIMEOUT),
185 put(boss_db_transaction_info, State),
186 {reply, Reply, _} = boss_db_controller:handle_call({transaction, TransactionFun}, self(), State),
187 put(boss_db_transaction_info, undefined),
188 poolboy:checkin(?POOLNAME, Worker),
189 Reply.
d036073 BossDB is now its own project
Evan Miller authored
190
191 %% @spec save_record( BossRecord ) -> {ok, SavedBossRecord} | {error, [ErrorMessages]}
192 %% @doc Save (that is, create or update) the given BossRecord in the database.
193 %% Performs validation first; see `validate_record/1'.
194 save_record(Record) ->
195 case validate_record(Record) of
196 ok ->
197 RecordId = Record:id(),
198 {IsNew, OldRecord} = if
199 RecordId =:= 'id' ->
200 {true, Record};
201 true ->
202 case find(RecordId) of
203 {error, _Reason} -> {true, Record};
204 undefined -> {true, Record};
205 FoundOldRecord -> {false, FoundOldRecord}
206 end
207 end,
208 HookResult = case boss_record_lib:run_before_hooks(Record, IsNew) of
209 ok -> {ok, Record};
210 {ok, Record1} -> {ok, Record1};
211 {error, Reason} -> {error, Reason}
212 end,
213 case HookResult of
214 {ok, PossiblyModifiedRecord} ->
a678c25 Fix transactions
Evan Miller authored
215 case db_call({save_record, PossiblyModifiedRecord}) of
d036073 BossDB is now its own project
Evan Miller authored
216 {ok, SavedRecord} ->
217 boss_record_lib:run_after_hooks(OldRecord, SavedRecord, IsNew),
218 {ok, SavedRecord};
219 Err -> Err
220 end;
221 Err -> Err
222 end;
223 Err -> Err
224 end.
225
226 %% @spec validate_record( BossRecord ) -> ok | {error, [ErrorMessages]}
227 %% @doc Validate the given BossRecord without saving it in the database.
228 %% `ErrorMessages' are generated from the list of tests returned by the BossRecord's
229 %% `validation_tests/0' function (if defined). The returned list should consist of
230 %% `{TestFunction, ErrorMessage}' tuples, where `TestFunction' is a fun of arity 0
231 %% that returns `true' if the record is valid or `false' if it is invalid.
232 %% `ErrorMessage' should be a (constant) string which will be included in `ErrorMessages'
233 %% if the `TestFunction' returns `false' on this particular BossRecord.
234 validate_record(Record) ->
235 Type = element(1, Record),
eeb78f0 Validate model parameter types
Evan Miller authored
236 Errors1 = case validate_record_types(Record) of
237 ok -> [];
238 {error, Errors} -> Errors
d036073 BossDB is now its own project
Evan Miller authored
239 end,
eeb78f0 Validate model parameter types
Evan Miller authored
240 Errors2 = case Errors1 of
241 [] ->
242 case erlang:function_exported(Type, validation_tests, 1) of
243 true -> [String || {TestFun, String} <- Record:validation_tests(), not TestFun()];
244 false -> []
245 end;
246 _ -> Errors1
247 end,
248 case length(Errors2) of
d036073 BossDB is now its own project
Evan Miller authored
249 0 -> ok;
eeb78f0 Validate model parameter types
Evan Miller authored
250 _ -> {error, Errors2}
251 end.
252
253 %% @spec validate_record_types( BossRecord ) -> ok | {error, [ErrorMessages]}
254 %% @doc Validate the parameter types of the given BossRecord without saving it
255 %% to the database.
256 validate_record_types(Record) ->
257 Errors = lists:foldl(fun
258 ({Attr, Type}, Acc) ->
259 Data = Record:Attr(),
260 GreatSuccess = case {Data, Type} of
d44c938 Cast attribute values to specified types
Evan Miller authored
261 {undefined, _} ->
262 true;
eeb78f0 Validate model parameter types
Evan Miller authored
263 {Data, string} when is_list(Data) ->
264 true;
265 {Data, binary} when is_binary(Data) ->
266 true;
267 {{{D1, D2, D3}, {T1, T2, T3}}, datetime} when is_integer(D1), is_integer(D2), is_integer(D3),
268 is_integer(T1), is_integer(T2), is_integer(T3) ->
269 true;
270 {Data, integer} when is_integer(Data) ->
271 true;
272 {Data, float} when is_float(Data) ->
273 true;
d44c938 Cast attribute values to specified types
Evan Miller authored
274 {Data, boolean} when is_boolean(Data) ->
eeb78f0 Validate model parameter types
Evan Miller authored
275 true;
d44c938 Cast attribute values to specified types
Evan Miller authored
276 {{N1, N2, N3}, timestamp} when is_integer(N1), is_integer(N2), is_integer(N3) ->
eeb78f0 Validate model parameter types
Evan Miller authored
277 true;
278 {_Data, Type} ->
279 false
280 end,
281 if
282 GreatSuccess ->
283 Acc;
284 true ->
285 [lists:concat(["Invalid data type for ", Attr])|Acc]
286 end
287 end, [], Record:attribute_types()),
288 case Errors of
289 [] -> ok;
d036073 BossDB is now its own project
Evan Miller authored
290 _ -> {error, Errors}
291 end.
292
293 %% @spec type( Id::string() ) -> Type::atom()
294 %% @doc Returns the type of the BossRecord with `Id', or `undefined' if the record does not exist.
295 type(Key) ->
296 case find(Key) of
297 undefined -> undefined;
298 Record -> element(1, Record)
299 end.
300
301 data_type(_, _Val) when is_float(_Val) ->
302 "float";
303 data_type(_, _Val) when is_binary(_Val) ->
304 "binary";
305 data_type(_, _Val) when is_integer(_Val) ->
306 "integer";
307 data_type(_, _Val) when is_tuple(_Val) ->
308 "datetime";
309 data_type(_, _Val) when is_boolean(_Val) ->
310 "boolean";
311 data_type(_, undefined) ->
312 "null";
313 data_type('id', _) ->
314 "id";
315 data_type(Key, Val) when is_list(Val) ->
316 case lists:suffix("_id", atom_to_list(Key)) of
317 true -> "foreign_id";
318 false -> "string"
319 end.
320
321 normalize_conditions(Conditions) ->
322 normalize_conditions(Conditions, []).
323
324 normalize_conditions([], Acc) ->
325 lists:reverse(Acc);
326 normalize_conditions([Key, Operator, Value|Rest], Acc) when is_atom(Key), is_atom(Operator) ->
327 normalize_conditions(Rest, [{Key, Operator, Value}|Acc]);
328 normalize_conditions([{Key, Value}|Rest], Acc) when is_atom(Key) ->
329 normalize_conditions(Rest, [{Key, 'equals', Value}|Acc]);
330 normalize_conditions([{Key, Operator, Value}|Rest], Acc) when is_atom(Key), is_atom(Operator) ->
331 normalize_conditions(Rest, [{Key, Operator, Value}|Acc]).
Something went wrong with that request. Please try again.