Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

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