PageRenderTime 49ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/ce/src/ce_db.erl

https://github.com/bmizerany/jungerl
Erlang | 274 lines | 140 code | 38 blank | 96 comment | 0 complexity | e2c044d8005148c7f0e0d398f300f964 MD5 | raw file
Possible License(s): LGPL-2.1, BSD-3-Clause, AGPL-1.0
  1. %%% BEGIN ce_db.erl %%%
  2. %%%
  3. %%% ce - Miscellaneous Programming Support Libraries for Erlang/OTP
  4. %%% Copyright (c)2003 Cat's Eye Technologies. All rights reserved.
  5. %%%
  6. %%% Redistribution and use in source and binary forms, with or without
  7. %%% modification, are permitted provided that the following conditions
  8. %%% are met:
  9. %%%
  10. %%% Redistributions of source code must retain the above copyright
  11. %%% notice, this list of conditions and the following disclaimer.
  12. %%%
  13. %%% Redistributions in binary form must reproduce the above copyright
  14. %%% notice, this list of conditions and the following disclaimer in
  15. %%% the documentation and/or other materials provided with the
  16. %%% distribution.
  17. %%%
  18. %%% Neither the name of Cat's Eye Technologies nor the names of its
  19. %%% contributors may be used to endorse or promote products derived
  20. %%% from this software without specific prior written permission.
  21. %%%
  22. %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  23. %%% CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
  24. %%% INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
  25. %%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  26. %%% DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
  27. %%% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
  28. %%% OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  29. %%% PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
  30. %%% OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
  31. %%% ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  32. %%% OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  33. %%% OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  34. %%% POSSIBILITY OF SUCH DAMAGE.
  35. %% @doc Database library.
  36. %%
  37. %% <p>This library complements mnesia. It uses mnesia to access the
  38. %% databases, using a simpler (but less efficient) interface.</p>
  39. %%
  40. %% @end
  41. -module(ce_db).
  42. -vsn('JUNGERL').
  43. -author('catseye@catseye.mb.ca').
  44. -copyright('Copyright (c)2003 Cat`s Eye Technologies. All rights reserved.').
  45. -export([create/1, connect/3]).
  46. -export([probe_db_nodes/1]).
  47. -export([read/2, read/3, write/2, delete/2]).
  48. -export([foreach/2, fold/2, fold/3, all_records/1]).
  49. -export([update_counter/2]).
  50. -export([export/3, import/3]).
  51. %%% DB MANAGEMENT %%%
  52. %% @spec create(Tables::[tabledef()]) -> ok | {error, Reason}
  53. %% tabledef() = {table(), [option()]}
  54. %% @doc Creates a schema with the given tables if one does not already exist.
  55. %% This schema is created on the local node only.
  56. %% If a schema already exists on the local node,
  57. %% the given tables are added to it. See the <code>mnesia</code> documentation
  58. %% for an explanation of the <code>tabledef()</code> structure.
  59. create(Tables) ->
  60. mnesia:stop(),
  61. Node = node(),
  62. (catch mnesia:create_schema([Node])),
  63. mnesia:start(),
  64. lists:foreach(fun({Table, Properties}) ->
  65. (catch mnesia:create_table(Table, Properties))
  66. end, Tables),
  67. ok.
  68. %% @spec connect(Master::node(), RamTables::[table()], DiscTables::[table()]) -> ok | {error, Reason}
  69. %% @doc Connects to a database hosted on a remote node.
  70. %% This is useful for connecting slave nodes to a master after the
  71. %% database has been created on the master node using <code>ce_db:create/1</code>.
  72. %% This connection needs only be done once, at install time, not each time
  73. %% the application is started. If the connection is successful, local copies of
  74. %% the given tables from the master node will be made in RAM and/or on
  75. %% disc on the local node.
  76. connect(MasterNode, RamTables, DiscTables) ->
  77. mnesia:start(),
  78. Node = node(),
  79. spawn(MasterNode, ?MODULE, probe_db_nodes, [self()]),
  80. receive
  81. {probe_db_nodes_reply, DBNodes} ->
  82. mnesia:change_config(extra_db_nodes, DBNodes),
  83. mnesia:change_table_copy_type(schema, Node, disc_copies),
  84. lists:foreach(fun(X) ->
  85. mnesia:add_table_copy(X, Node, ram_copies)
  86. end, RamTables),
  87. lists:foreach(fun(X) ->
  88. mnesia:add_table_copy(X, Node, disc_copies)
  89. end, DiscTables),
  90. ok;
  91. Else ->
  92. {error, Else}
  93. end.
  94. %% @spec probe_db_nodes(pid()) -> ok
  95. %% @doc Used by <code>connect/3</code> to probe the remote node.
  96. %% This function is invoked on the remote (master) node, and sends a message
  97. %% back to the local (slave) node.
  98. probe_db_nodes(ForPid) ->
  99. ForPid ! {probe_db_nodes_reply, mnesia:system_info(db_nodes)},
  100. ok.
  101. %% @spec read(table(), key()) -> record() | nil
  102. %% table() = atom()
  103. %% key() = term()
  104. %% @doc Reads the record with a matching primary key from the table.
  105. %% Returns nil if no record was found.
  106. read(Table, Key) ->
  107. case mnesia:transaction(fun() ->
  108. mnesia:read(Table, Key, read)
  109. end) of
  110. {atomic, [H | T]} -> H;
  111. _ -> nil
  112. end.
  113. %% @spec read(table(), index(), key()) -> [record()]
  114. %% index() = atom()
  115. %% @doc Reads all records with a matching secondary key from the table.
  116. %% Returns an empty list if none were found.
  117. read(Table, Index, Key) ->
  118. case mnesia:transaction(fun() ->
  119. mnesia:index_read(Table, Key, Index)
  120. end) of
  121. {atomic, List} -> List;
  122. _ -> []
  123. end.
  124. %% @spec write(table(), record()) -> true | false
  125. %% @doc Inserts or updates a single record in a data table.
  126. write(Table, Object) ->
  127. case mnesia:transaction(fun() ->
  128. mnesia:write(Table, Object, write)
  129. end) of
  130. {atomic, ok} -> true;
  131. Else -> false
  132. end.
  133. %% @spec delete(table(), key()) -> true | false
  134. %% @doc Deletes a single record from a data table.
  135. delete(Table, Key) ->
  136. case mnesia:transaction(fun() ->
  137. mnesia:delete(Table, Key, write)
  138. end) of
  139. {atomic, ok} -> true;
  140. Else -> false
  141. end.
  142. %% @spec foreach(table(), fun()) -> true | false
  143. %% @doc Traverses an entire table, calling fun/2 for each record.
  144. %% The result of the evaluation of the fun is discarded.
  145. foreach(Table, Fun) ->
  146. case mnesia:transaction(fun() ->
  147. mnesia:foldl(Fun, [], Table), ok
  148. end) of
  149. {atomic, ok} -> true;
  150. Else -> false
  151. end.
  152. %% @spec fold(table(), fun()) -> {ok, term()} | {error, Reason}
  153. %% @equiv fold(Table, Fun, [])
  154. fold(Table, Fun) ->
  155. fold(Table, Fun, []).
  156. %% @spec fold(table(), fun(), term()) -> {ok, term()} | {error, Reason}
  157. %% @doc Folds an entire table, calling fun/2 for each record.
  158. fold(Table, Fun, Acc) ->
  159. case mnesia:transaction(fun() ->
  160. mnesia:foldl(Fun, Acc, Table)
  161. end) of
  162. {atomic, Acc0} -> {ok, Acc0};
  163. Else -> Else
  164. end.
  165. %% @spec all_records(table()) -> [record()]
  166. %% @doc Returns a list of all the records in the table.
  167. %% Complements mnesia:all_keys/1. Note that this is not a particularly
  168. %% scaleable solution.
  169. all_records(Table) ->
  170. Arity = mnesia:table_info(Table, arity),
  171. Template = list_to_tuple([Table | lists:duplicate(Arity - 1, '_')]),
  172. case mnesia:transaction(fun() ->
  173. mnesia:match_object(Template)
  174. end) of
  175. {atomic, List} -> List;
  176. Else -> []
  177. end.
  178. %% @spec update_counter(table(), key()) -> {ok, integer()} | {error, Reason}
  179. %% @doc Increments an integer value in a table.
  180. %% Complements mnesia:dirty_update_counter/2. This function is much less
  181. %% efficient, but it is cleaner.
  182. update_counter(Table, Key) ->
  183. case mnesia:transaction(fun() ->
  184. [{T,K,Number}] = mnesia:read(Table, Key, read),
  185. mnesia:write(Table, {T,K,Number + 1}, write),
  186. Number + 1
  187. end) of
  188. {atomic, N} -> {ok, N};
  189. Else -> Else
  190. end.
  191. %% @spec export(io_device(), table(), FieldNames::[atom()]) -> ok | {error, Reason}
  192. %% @doc Exports a table to a disk file. For simplicity,
  193. %% FieldNames should be the output of <code>record_info(fields, Table)</code>
  194. export(IoDevice, Table, FieldNames) ->
  195. io:fwrite(IoDevice, "~p.~n", [Table]),
  196. fold(Table, fun(Record, ok) ->
  197. io:fwrite(IoDevice, "~p.~n", [map_fields(Record, FieldNames)])
  198. end, ok),
  199. io:fwrite(IoDevice, "~p.~n", [end_of_records]).
  200. map_fields(Record, FieldNames) ->
  201. map_fields(tl(tuple_to_list(Record)), FieldNames, []).
  202. map_fields([], [], Acc) -> Acc;
  203. map_fields([Field | Fields], [FieldName | FieldNames], Acc) ->
  204. map_fields(Fields, FieldNames, [{FieldName, Field} | Acc]).
  205. %% @spec import(io_device(), table(), FieldNames::[atom()]) -> ok | {error, Reason}
  206. %% @doc Imports a disk file into a table. For simplicity,
  207. %% FieldNames should be the output of <code>record_info(fields, Table)</code>
  208. import(IoDevice, Table, FieldNames) ->
  209. case io:read(IoDevice, '') of
  210. {ok, Table} ->
  211. import_loop(IoDevice, Table, FieldNames);
  212. _ ->
  213. {error, could_not_read_table_tag}
  214. end.
  215. import_loop(IoDevice, Table, FieldNames) ->
  216. case io:read(IoDevice, '') of
  217. {ok, end_of_records} ->
  218. ok;
  219. {ok, TupleList} ->
  220. Record = map_record(Table, TupleList, FieldNames),
  221. write(Table, Record),
  222. import(IoDevice, Table, FieldNames)
  223. end.
  224. map_record(Table, TupleList, FieldNames) ->
  225. list_to_tuple([Table | map_record0(TupleList, FieldNames, [])]).
  226. map_record0(TupleList, [], Acc) ->
  227. lists:reverse(Acc);
  228. map_record0(TupleList, [FieldName | FieldNames], Acc) ->
  229. case lists:keysearch(FieldName, 1, TupleList) of
  230. {value, {FieldName, FieldValue}} ->
  231. map_record0(TupleList, FieldNames, [FieldValue | Acc]);
  232. error ->
  233. % should fill in the default value, will fill in atom 'default'
  234. map_record0(TupleList, FieldNames, [default | Acc])
  235. end.
  236. %%% END of ce_db.erl %%%