%% @author Matthew Pflueger (matthew.pflueger@gmail.com)
%% @copyright Matthew Pflueger 2007
%% @doc This module implements the Mnesia driver for ErlyDB.
%%
%% This is an internal ErlyDB module that you normally shouldn't have to
%% use directly. For most situations, all you have to know
%% about this module is the options you can pass to {@link start/1}, which
%% is called by {@link erlydb:start/2}. Currently (Erlyweb 0.6), no options are
%% recognized/used.
%%
%%
%% == Contents ==
%%
%% {@section Introduction}
%% {@section Conventions}
%% {@section Types}
%% {@section Example}
%% {@section What's Not Supported}
%%
%%
%% == Introduction ==
%%
%% Mnesia is Erlang's distributed DataBase Management System (DBMS). Please read the
%% Mnesia Reference Manual for more information about Mnesia.
%%
%% This driver executes Erlsql queries against Mnesia. Most Erlsql queries are
%% dynamically converted into Query List Comprehension (QLC) expressions before
%% execution. Please see the qlc module documentation for more information on QLC.
%% Please read the Erlsql documentation for more information on Erlsql.
%%
%% This driver does not add relational support to Mnesia (constraints,
%% cascades, etc). Some relational support for Mnesia has been implemented by
%% Ulf Wiger in the user contribution rdbms (see http://erlang.org/user.html).
%% For more information visit http://ulf.wiger.net/rdbms/doc/rdbms.html. You can download a more
%% recent version of rdbms at http://ulf.wiger.net/rdbms/download/.
%%
%%
%% == Conventions ==
%%
%% The driver uses a table named 'counter' for auto-incrementing (identity) primary key columns.
%% (only valid for set or ordered-set tables). The 'counter' table must be created
%% using the following:
%%
%% mnesia:create_table(counter, [{disc_copies, [node()]}, {attributes, [key, counter]}])
%%
%% The key column will contain table names of the mnesia tables utilizing identity columns. The
%% counter contains the value of the last used identity (serial integer). The counter is updated
%% using:
%%
%% mnesia:dirty_update_counter(counter, Table, 1)
%%
%% You can initialize/start the identity of a particular table by executing the above statement
%% with an arbitrary number (greater than 0). The above operation is atomic (the function name
%% is misleading). Please read the Mnesia docs for more information. The use of the 'counter'
%% table is currently not customizable but that will hopefully change soon.
%%
%% All columns named 'id' or ending with 'id' are treated as integers. If the column named 'id'
%% is the first attribute (column) in the mnesia table, then it is also treated as an
%% auto-incrementing identity column.
%%
%%
%% == Types ==
%%
%% This driver stores all fields as binary unless the field name ends with id and in that
%% case the field is treated as an integer (as discussed above). This can be customized
%% by utilizing the user_properties for a mnesia table. The driver will do a limited
%% amount of type conversion utilizing these properties. The driver will recognize
%% user_properties for a field if defined in the following format:
%%
%% {Field, {Type, Modifier}, Null, Key, Default, Extra, MnesiaType}
%%
%% where Field is an atom and must be the same as the field (attribute) name,
%% Type through Extra is are as defined in erlydb_field:new/6
%% MnesiaType is the type to store the field as in mnesia.
%%
%% Currently, only the following values for MnesiaType are recognized:
%%
%% atom, list, binary, integer, float, datetime, date, time, undefined
%%
%% The erlydb_mnesia driver will attempt to convert field values into
%% the specified type before insertion/update/query of the record in
%% mnesia... If the MnesiaType has a value of undefined then no type
%% conversion is attempted for the field.
%%
%%
%% == Example ==
%%
%% Given the following record:
%%
%% -record(person, {myid, type, name, age, country, office, department, genre, instrument, created_on})
%%
%% Create a Mnesia table with types for the driver using:
%%
%% {atomic, ok} = mnesia:create_table(person, [
%% {disc_copies, [node()]},
%% {attributes, record_info(fields, person)},
%% {user_properties, [{myid, {integer, undefined}, false, primary, undefined, identity, integer},
%% {type, {varchar, undefined}, false, undefined, undefined, undefined, atom},
%% {age, {integer, undefined}, true, undefined, undefined, undefined, integer},
%% {created_on, {datetime, undefined}, true, undefined, undefined, undefined, undefined}]}])
%%
%% Note the following:
%% 1) The primary key column is called myid and is an auto-incrementing integer column. This is the
%% same as if the column had been named 'id'.
%% 2) The type and age columns have customized types. The driver will try to convert all values
%% inserted into the table into the specified types.
%% 3) The created_on column is defined as a datetime for Erlyweb but is of type undefined for the
%% Mnesia driver. This means that no type conversion will be attempted for the created_on
%% column resulting in a Erlang datetime tuple to be stored in the column
%% {{Year, Month, Day}, {Hour, Minute, Second}} or {datetime, {{Year, Month, Day},{Hour,Minute,Second}}}
%% depending on how you create the record (creating a record from strings will result in the
%% tuple beginning with datetime).
%% 4) Changing the user property for the created_on column to specify a mnesia type of datetime like
%% {created_on, {datetime, undefined}, true, undefined, undefined, undefined, datetime}
%% will result in the erlang date time tuple {{Year,Month,Day},{Hour,Minute,Second}}
%% to be stored regardless of how the record was created (ie. it will strip the redundant
%% datetime atom from the tuple
%%
%%
%% See test/erlydb/erlydb_mnesia_schema for more examples of how to create mnesia tables
%% with user_properties...
%%
%%
%% == What's Not Supported ==
%%
%% This driver is very much still alpha quality. Much is not supported but the most glaring
%% are unions and sub-queries.
%%
%% For license information see LICENSE.txt
-module(erlydb_mnesia).
-author("Matthew Pflueger (matthew.pflueger@gmail.com)").
-export([start/0,
start/1,
get_metadata/1,
q/1,
q/2,
transaction/2,
select/2,
select_as/3,
update/2,
get_last_insert_id/2]).
%% Useful for debugging
-define(L(Msg), io:format("~p:~b ~p ~n", [?MODULE, ?LINE, Msg])).
-define(S(Obj), io:format("LOG ~w ~s\n", [?LINE, Obj])).
-record(qhdesc, {expressions = [],
generators = [],
filters = [],
bindings = erl_eval:new_bindings(),
options = [],
evalfun = fun qlc:e/2,
postqh = fun postqh/2,
posteval = fun posteval/1,
metadata = dict:new()}).
start() ->
case mnesia:system_info(is_running) of
no -> mnesia:start();
_ -> ok
% FIXME this could fail if system_info returns 'stopping'
end.
%% @doc Start the Mnesia driver using the options property list. Currently, no options are recognized.
%%
%% @spec start(StartOptions::proplist()) -> ok | {error, Error}
start(_Options) ->
ok = start().
%% @doc Get the table names and fields for the database.
%%
%% @spec get_metadata(Options::proplist()) -> gb_trees()
get_metadata(_Options) ->
% NOTE Integration with mnesia_rdbms would be interesting...
Tables = mnesia:system_info(tables) -- [schema],
Tree = lists:foldl(
fun(Table, TablesTree) ->
gb_trees:enter(Table, get_metadata(Table, table_fields(Table)), TablesTree)
end, gb_trees:empty(), Tables),
{ok, Tree}.
get_metadata(Table, Fields) when is_list(Fields) ->
[get_metadata(Table, Field) || Field <- Fields];
get_metadata(Table, Field) ->
{Field, {Type, Modifier}, Null, Key, Default, Extra, _MnesiaType} = get_user_properties(Table, Field),
erlydb_field:new(Field, {Type, Modifier}, Null, Key, Default, Extra).
q(Statement) ->
q(Statement, undefined).
q({esql, Statement}, Options) ->
?L(["In q with: ", Statement]),
q2(Statement, Options);
q(Statement, Options) when is_binary(Statement); is_list(Statement) ->
?L(["Unhandled binary or list query", Statement, Options]),
exit("Unhandled binary or list query").
q2({select, {call, count, _What}, {from, Table}, {where, undefined}, undefined}, _Options)
when is_list(Table) == false ->
{ok, [{table_size(Table)}]};
q2({select, Table}, Options) when is_list(Table) == false ->
q2({select, [Table]}, Options);
q2({select, Tables}, Options) when is_list(Tables) ->
{atomic, Results} = transaction(fun() -> lists:foldl(
fun(Table, Acc) -> Acc ++ mnesia:match_object(mnesia:table_info(Table, wild_pattern)) end,
[], Tables) end, Options),
{data, Results};
q2({select, '*', {from, Tables}}, Options) ->
q2({select, Tables}, Options);
q2({select, '*', {from, Tables}, {where, undefined}, undefined}, Options) ->
q2({select, Tables}, Options);
%% QLC queries
q2({select, Fields, {from, Tables}}, Options) ->
select(undefined, Fields, Tables, undefined, undefined, Options);
q2({select, Fields, {from, Tables}, {where, WhereExpr}}, Options) ->
select(undefined, Fields, Tables, WhereExpr, undefined, Options);
q2({select, Fields, {from, Tables}, {where, WhereExpr}, Extras}, Options) ->
select(undefined, Fields, Tables, WhereExpr, Extras, Options);
q2({select, Fields, {from, Tables}, WhereExpr, Extras}, Options) ->
select(undefined, Fields, Tables, WhereExpr, Extras, Options);
q2({select, Fields, {from, Tables}, Extras}, Options) ->
select(undefined, Fields, Tables, undefined, Extras, Options);
q2({select, Tables, {where, WhereExpr}}, Options) ->
select(undefined, undefined, Tables, WhereExpr, undefined, Options);
q2({select, Tables, WhereExpr}, Options) ->
select(undefined, undefined, Tables, WhereExpr, undefined, Options);
q2({select, Modifier, Fields, {from, Tables}}, Options) ->
select(Modifier, Fields, Tables, undefined, undefined, Options);
q2({select, Modifier, Fields, {from, Tables}, {where, WhereExpr}}, Options) ->
select(Modifier, Fields, Tables, WhereExpr, undefined, Options);
q2({select, Modifier, Fields, {from, Tables}, Extras}, Options) ->
select(Modifier, Fields, Tables, undefined, Extras, Options);
q2({select, Modifier, Fields, {from, Tables}, {where, WhereExpr}, Extras}, Options) ->
select(Modifier, Fields, Tables, WhereExpr, Extras, Options);
q2({select, Modifier, Fields, {from, Tables}, WhereExpr, Extras}, Options) ->
select(Modifier, Fields, Tables, WhereExpr, Extras, Options);
%% q2({Select1, union, Select2}, Options) ->
%% [$(, q2(Select1, Options), <<") UNION (">>, q2(Select2, Options), $)];
%% q2({Select1, union, Select2, {where, WhereExpr}}, Options) ->
%% [q2({Select1, union, Select2}, Options), where(WhereExpr, Options)];
%% q2({Select1, union, Select2, Extras}, Options) ->
%% [q2({Select1, union, Select2}, Options), extra_clause(Extras, Options)];
%% q2({Select1, union, Select2, {where, _} = Where, Extras}, Options) ->
%% [q2({Select1, union, Select2, Where}, Options), extra_clause(Extras, Options)];
q2({insert, Table, Params}, Options) ->
{Fields, Values} = lists:unzip(Params),
q2({insert, Table, Fields, [Values]}, Options);
q2({insert, Table, Fields, ValuesList}, _Options) ->
{Fields1, ValuesList1} =
case get_identity_field(Table) of
undefined -> {Fields, ValuesList};
Field -> NewFields = [Field | Fields],
MaxId = mnesia:dirty_update_counter(counter, Table, length(ValuesList)),
put(mnesia_last_insert_id, MaxId),
{NewValuesList, _} =
lists:mapfoldr(fun(Values, Id) -> {[Id | Values], Id-1} end, MaxId, ValuesList),
{NewFields, NewValuesList}
end,
QLCData = get_qlc_metadata(Table),
lists:foreach(fun(Values) ->
ok = write(dict:fetch({new_record, Table}, QLCData), Fields1, Values, QLCData)
end, ValuesList1),
{ok, length(ValuesList1)};
q2({update, Table, Params}, _Options) ->
{Fields, Values} = lists:unzip(Params),
QLCData = get_qlc_metadata(Table),
TraverseFun = fun(Record, {Fields1, Values1, QLCData1}) ->
write(Record, Fields1, Values1, QLCData1),
{Fields1, Values1, QLCData1}
end,
{atomic, _} = traverse(TraverseFun, {Fields, Values, QLCData}, Table),
{ok, table_size(Table)};
q2({update, Table, Params, {where, Where}}, Options) ->
q2({update, Table, Params, Where}, Options);
q2({update, Table, Params, Where}, Options) ->
QHDesc = #qhdesc{metadata = get_qlc_metadata(Table)},
{Fields, Values} = lists:unzip(Params),
QLCData = QHDesc#qhdesc.metadata,
{atomic, Num} = mnesia:transaction(
fun() ->
{data, Records} = select(undefined, undefined, Table, Where, undefined, Options, QHDesc),
lists:foreach(fun(Record) -> write(Record, Fields, Values, QLCData) end, Records),
length(Records)
end),
{ok, Num};
q2({delete, {from, Table}, {where, undefined}}, Options) ->
q2({delete, Table}, Options);
q2({delete, {from, Table}}, Options) ->
q2({delete, Table}, Options);
q2({delete, Table}, _Options) ->
% cannot use mnesia:clear_table(Table) here because sometimes this gets called inside a transaction...
TraverseFun = fun(Record, Num) ->
mnesia:delete_object(Record),
Num + 1
end,
{atomic, Num} = traverse(TraverseFun, 0, Table),
{ok, Num};
q2({delete, {from, Table}, {where, Where}}, Options) ->
q2({delete, Table, Where}, Options);
q2({delete, Table, {where, Where}}, Options) ->
q2({delete, Table, Where}, Options);
q2({delete, Table, Where}, Options) ->
{atomic, Num} = mnesia:transaction(
fun() ->
{data, Records} = q2({select, Table, Where}, Options),
lists:foreach(fun(Record) -> mnesia:delete_object(Record) end, Records),
length(Records)
end),
{ok, Num};
q2(Statement, Options) ->
?L(["Unhandled statement and options: ", Statement, Options]),
exit("Unhandled statement").
select(Modifier, Fields, Tables, WhereExpr, Extras, Options) ->
QHDesc = #qhdesc{metadata = get_qlc_metadata(Tables)},
select(Modifier, Fields, Tables, WhereExpr, Extras, Options, QHDesc).
select(Modifier, Fields, Tables, WhereExpr, Extras, Options, QHDesc) ->
QHDesc1 = modifier(Modifier, QHDesc),
QHDesc2 = fields(Fields, QHDesc1),
QHDesc3 = tables(Tables, QHDesc2),
QHDesc4 = where(WhereExpr, QHDesc3),
QHDesc5 = extras(Extras, QHDesc4),
Desc = QHDesc5,
QLC = if length(Desc#qhdesc.expressions) > 1 -> "[{" ++ comma(Desc#qhdesc.expressions) ++ "}";
true -> "[" ++ comma(Desc#qhdesc.expressions)
end,
QLC1 = QLC ++ " || " ++ comma(Desc#qhdesc.generators ++ lists:reverse(Desc#qhdesc.filters)) ++ "].",
?L(["About to execute QLC: ", QLC1]),
{atomic, Results} = transaction(
fun() ->
QHOptions = Desc#qhdesc.options,
QH = qlc:string_to_handle(QLC1, QHOptions, Desc#qhdesc.bindings),
PostQH = Desc#qhdesc.postqh,
QH1 = PostQH(QH, QHOptions),
EvalFun = Desc#qhdesc.evalfun,
EvalFun(QH1, QHOptions)
end, Options),
?L(["Found Results: ", Results]),
PostEval = Desc#qhdesc.posteval,
PostEval(Results).
modifier(undefined, #qhdesc{} = QHDesc) ->
QHDesc;
modifier(distinct, #qhdesc{options = Options} = QHDesc) ->
QHDesc#qhdesc{options = [{unique_all, true} | Options]};
modifier(Other, _QHDesc) ->
?L(["Unhandled modifier: ", Other]),
exit("Unhandled modifier").
fields(undefined, QHDesc) ->
fields('*', QHDesc);
fields('*', #qhdesc{expressions = Fields, metadata = QLCData} = QHDesc) ->
QHDesc#qhdesc{expressions = dict:fetch(aliases, QLCData) ++ Fields};
fields({call, avg, Field}, #qhdesc{metadata = QLCData} = QHDesc) when is_atom(Field) ->
[Table | _Tables] = dict:fetch(tables, QLCData),
fields({call, avg, {Table, Field}}, QHDesc);
fields({call, avg, {Table, Field}}, #qhdesc{metadata = QLCData} = QHDesc) ->
Index = dict:fetch({index,Table,Field}, QLCData),
QHDesc1 = QHDesc#qhdesc{posteval =
fun(Results) ->
Total = lists:foldl(fun(Record, Sum) -> element(Index, Record) + Sum end, 0, Results),
{ok, [{Total/length(Results)}]}
end},
fields('*', QHDesc1);
%% Count functions
fields({call, count, What}, #qhdesc{metadata = QLCData} = QHDesc) when is_atom(What) ->
QHDesc1 = case regexp:split(atom_to_list(What), "distinct ") of
{ok, [[], Field]} ->
[Table | _] = resolve_field(Field, QLCData),
QHDesc#qhdesc{
expressions = [dict:fetch({alias, Table}, QLCData)],
postqh = fun(QH, _QHOptions) ->
qlc:keysort(dict:fetch({index,Table,list_to_atom(Field)}, QLCData), QH, [{unique, true}])
end};
_Other -> fields('*', QHDesc)
end,
QHDesc1#qhdesc{posteval = fun count/1};
fields({call, count, _What}, QHDesc) ->
fields('*', QHDesc#qhdesc{posteval = fun count/1});
%% Max/Min functions
fields({call, max, Field}, QHDesc) ->
min_max(Field, QHDesc, [{order, descending}, {unique, true}]);
fields({call, min, Field}, QHDesc) ->
min_max(Field, QHDesc, [{unique, true}]);
fields([Field | Fields], QHDesc) ->
fields(Fields, fields(Field, QHDesc));
fields([], QHDesc) ->
QHDesc#qhdesc{expressions = lists:reverse(QHDesc#qhdesc.expressions)};
fields(Field, #qhdesc{metadata = QLCData} = QHDesc) when is_tuple(Field) == false ->
[Table | _Tables] = dict:fetch(tables, QLCData),
fields({Table,Field}, QHDesc);
fields({_,_} = Field, #qhdesc{expressions = Fields, metadata = QLCData} = QHDesc) ->
QHDesc#qhdesc{expressions = [dict:fetch(Field, QLCData) | Fields]}.
tables(Table, QHDesc) when is_list(Table) == false ->
tables([Table], QHDesc);
tables([Table | Tables], #qhdesc{generators = Generators, metadata = QLCData} = QHDesc) ->
tables(Tables, QHDesc#qhdesc{generators = [dict:fetch(Table, QLCData) | Generators]});
tables([], #qhdesc{generators = Generators} = QHDesc) ->
QHDesc#qhdesc{generators = lists:reverse(Generators)}.
where({Where1, 'and', Where2}, QHDesc) ->
QHDesc1 = where(Where1, QHDesc),
where(Where2, QHDesc1);
where({'or', Where}, #qhdesc{filters = Filters} = QHDesc) when is_list(Where) ->
QHDesc1 = where(Where, QHDesc#qhdesc{filters = []}),
OrFilter = "(" ++ combinewith(" orelse ", QHDesc1#qhdesc.filters) ++ ")",
QHDesc1#qhdesc{filters = [OrFilter | Filters]};
where({'not', Where}, #qhdesc{filters = Filters} = QHDesc) ->
QHDesc1 = where(Where, QHDesc#qhdesc{filters = []}),
NotFilter = "false == (" ++ combinewith(" andalso ", QHDesc1#qhdesc.filters) ++ ")",
QHDesc1#qhdesc{filters = [NotFilter | Filters]};
where({'and', Where}, #qhdesc{filters = Filters} = QHDesc) ->
QHDesc1 = where(Where, QHDesc#qhdesc{filters = []}),
AndFilter = "(" ++ combinewith(" andalso ", QHDesc1#qhdesc.filters) ++ ")",
QHDesc1#qhdesc{filters = [AndFilter | Filters]};
where([Where | Rest], QHDesc) ->
where(Rest, where(Where, QHDesc));
where([], QHDesc) ->
QHDesc;
where({From, Op, To}, #qhdesc{metadata = QLCData} = QHDesc) when is_tuple(From) == false ->
[Table | _] = resolve_field(From, QLCData),
where({{Table,From}, Op, To}, QHDesc);
where({{_,_} = From, 'is', 'null'}, #qhdesc{filters = Filters, metadata = QLCData} = QHDesc) ->
QHDesc#qhdesc{filters = [dict:fetch(From, QLCData) ++ " == undefined" | Filters]};
where({{_,_} = From, 'like', To}, QHDesc) when is_binary(To) ->
where({From, 'like', erlang:binary_to_list(To)}, QHDesc);
where({{Table,Field} = From, 'like', To}, #qhdesc{filters = Filters, metadata = QLCData} = QHDesc) ->
{ok, To1, _RepCount} = regexp:gsub(To, "%", ".*"),
To2 = "\"^" ++ To1 ++ "$\"",
Filter = case mnesia_type(Table, Field) of
binary -> "erlang:binary_to_list(" ++ dict:fetch(From, QLCData) ++ ")";
_Other -> dict:fetch(From, QLCData)
end,
QHDesc#qhdesc{filters = ["regexp:first_match(" ++ Filter ++ ", " ++ To2 ++ ") /= nomatch" | Filters]};
where({{_, _} = From, '=', To}, QHDesc) ->
where({From, "==", To}, QHDesc);
where({{_, _} = From, Op, To}, QHDesc) when is_atom(Op) ->
where({From, atom_to_list(Op), To}, QHDesc);
where({{_, _} = From, Op, {Table, Field} = To}, #qhdesc{filters = Filters, metadata = QLCData} = QHDesc)
when is_atom(Table), is_atom(Field) ->
QHDesc#qhdesc{filters = [lists:concat([dict:fetch(From, QLCData), " ", Op, " ",
dict:fetch(To, QLCData)]) | Filters]};
where({{Table, Field} = From, Op, To}, #qhdesc{filters = Filters, bindings = Bindings, metadata = QLCData} = QHDesc) ->
case resolve_field(To, QLCData) of
[ToTable | _] -> where({From, Op, {ToTable, To}}, QHDesc);
[] -> Var = list_to_atom("Var" ++ integer_to_list(random:uniform(100000))),
QHDesc#qhdesc{
filters = [lists:concat([dict:fetch(From, QLCData), " ", Op, " ", Var]) | Filters],
bindings = erl_eval:add_binding(Var, convert(Table, Field, To), Bindings)}
end;
where(undefined, QHDesc) ->
QHDesc;
where(Where, _QHDesc) ->
?L(["Unhandled where: ", Where]),
exit("Unhandled where").
extras([Extra | Extras], QHDesc) ->
QHDesc1 = extras(Extra, QHDesc),
extras(Extras, QHDesc1);
extras([], QHDesc) ->
QHDesc;
extras({order_by, {Field, Order}}, #qhdesc{metadata = QLCData} = QHDesc) when is_atom(Field) ->
QHDesc#qhdesc{postqh =
fun(QH, QHOptions) ->
[Table | _Rest] = dict:fetch(tables, QLCData),
SortOptions = [{order, translate_order(Order)} | QHOptions],
qlc:keysort(dict:fetch({index,Table,Field}, QLCData), QH,
SortOptions)
end};
extras({limit, Limit}, QHDesc) ->
QHDesc#qhdesc{evalfun =
fun(QH, QHOptions) ->
QHCursor = qlc:cursor(QH, QHOptions),
Results = qlc:next_answers(QHCursor, Limit),
qlc:delete_cursor(QHCursor),
Results
end};
extras({limit, 0, Limit}, QHDesc) ->
extras({limit, Limit}, QHDesc);
extras({limit, From, Limit}, QHDesc) ->
QHDesc#qhdesc{evalfun =
fun(QH, QHOptions) ->
QHCursor = qlc:cursor(QH, QHOptions),
qlc:next_answers(QHCursor, From),
Results = qlc:next_answers(QHCursor, Limit),
qlc:delete_cursor(QHCursor),
Results
end};
extras(undefined, QHDesc) ->
QHDesc;
extras(Extras, _QHDesc) ->
?L(["Unhandled extras: ", Extras]),
exit("Unhandled extras").
translate_order(asc) -> ascending;
translate_order(desc) -> descending.
postqh(QueryHandle, _QHOptions) ->
QueryHandle.
posteval(Results) ->
{data, Results}.
count(Results) ->
{ok, [{length(Results)}]}.
min_max(Field, #qhdesc{metadata = QLCData} = QHDesc, Options) ->
[Table | _] = resolve_field(Field, QLCData),
QHDesc1 = QHDesc#qhdesc{postqh =
fun(QH, _QHOptions) ->
qlc:keysort(dict:fetch({index,Table,Field}, QLCData), QH, Options)
end},
QHDesc2 = QHDesc1#qhdesc{posteval =
fun(Results) ->
{ok, [{element(dict:fetch({index,Table,Field}, QLCData), hd(Results))}]}
end},
QHDesc2#qhdesc{expressions = [dict:fetch({alias, Table}, QLCData)]}.
% For each table, add the metadata for the table's fields to the dictionary and then
% add TABLE_ROW_VAR <- mnesia:table(Table) to the dictionary where TABLE_ROW_VAR is the variable
% representing the current row of the table and Table is the table name as an atom (TABLE_ROW_VAR
% defaults to the table name in all caps)
get_qlc_metadata(Table) when is_list(Table) == false ->
get_qlc_metadata([Table]);
get_qlc_metadata(Tables) when is_list(Tables) ->
QLCData = dict:store(tables, [], dict:new()),
QLCData1 = dict:store(aliases, [], QLCData),
get_qlc_metadata(Tables, QLCData1).
get_qlc_metadata([Table | Tables], QLCData) when is_tuple(Table) == false ->
% Create an alias for the table (table name in all caps)
get_qlc_metadata({Table, 'as', string:to_upper(atom_to_list(Table))}, Tables, QLCData);
get_qlc_metadata([{_, 'as', _} = Table | Tables], QLCData) ->
get_qlc_metadata(Table, Tables, QLCData);
get_qlc_metadata([], QLCData) ->
QLCData.
% For each table store the following key => value pairs:
% {alias, Table} => Alias where Table is atom and Alias is string
% {table, Alias} => Table where Table is atom and Alias is string
% {new_record, Table} => {Table, undefined, undefined...} where Table is atom and value is a tuple
% Table => MnesiaTable where Table is atom and MnesiaTable is the string "Alias <- mnesia:table(Table)"
%
% Also store:
% tables => [Tables] where Tables is a list of all tables in query
% aliases => [Aliases] where Aliases is a list of all table aliases in query
get_qlc_metadata({Table, 'as', Alias}, Tables, QLCData) ->
QLCData1 = dict:store({alias, Table}, Alias, QLCData),
QLCData2 = dict:store({table, Alias}, Table, QLCData1),
QLCData3 = dict:store({new_record, Table}, {Table}, QLCData2),
QLCData4 = get_qlc_metadata(table_fields(Table), 2, Table, Alias, QLCData3),
MnesiaTable = lists:concat([Alias, " <- mnesia:table(", Table, ")"]),
QLCData5 = dict:store(Table, MnesiaTable, QLCData4),
QLCData6 = dict:store(tables, dict:fetch(tables, QLCData5) ++ [Table], QLCData5),
QLCData7 = dict:store(aliases, dict:fetch(aliases, QLCData6) ++ [Alias], QLCData6),
get_qlc_metadata(Tables, QLCData7).
% for each table field (column), store the following:
% {Table, Field} => "element(Alias, FieldIndex)" where Table and Field are atoms
% {Alias, Field} => "element(Alias, FieldIndex)" where Alias is a string and Field is an atom
% {index, Table, Field} => FieldIndex where Table and Field are atoms and FieldIndex is an integer
get_qlc_metadata([Field | Fields], FieldIndex, Table, Alias, QLCData) ->
Data = "element(" ++ integer_to_list(FieldIndex) ++ ", " ++ Alias ++ ")",
QLCData1 = dict:store({Table,Field}, Data, QLCData),
QLCData2 = dict:store({Alias,Field}, Data, QLCData1),
QLCData3 = dict:store({index,Table,Field}, FieldIndex, QLCData2),
TableRecord = dict:fetch({new_record, Table}, QLCData3),
QLCData4 = dict:store({new_record, Table}, erlang:append_element(TableRecord, undefined), QLCData3),
get_qlc_metadata(Fields, FieldIndex + 1, Table, Alias, QLCData4);
get_qlc_metadata([], _FieldIndex, _Table, _Alias, QLCData) ->
QLCData.
%% User_properties for field is defined as:
%% {Field, {Type, Modifier}, Null, Key, Default, Extra, MnesiaType}
%% where Field is an atom,
%% Type through Extra is are as defined in erlydb_field:new/6
%% MnesiaType is the type to store the field as in mnesia.
%%
%% Currently the driver tries to do a limited bit of conversion of types. For example, you may want
%% to store strings as binaries in mnesia. Erlydb may pass in strings during querying, updates, etc
%% and the string will need to be converted to/from a binary.
get_user_properties(Table, Field) ->
case lists:keysearch(Field, 1, mnesia:table_info(Table, user_properties)) of
{value, {Field, {_Type, _Modifier}, _Null, _Key, _Default, _Extra, _MnesiaType} = UserProperties} -> UserProperties;
false -> get_default_user_properties(table_type(Table), Field, field_index(Table, Field))
end.
get_default_user_properties(TableType, Field, 1) ->
case lists:suffix("id", atom_to_list(Field)) of
true -> if TableType =:= bag -> {Field, {integer, undefined}, false, primary, undefined, undefined, integer};
true -> {Field, {integer, undefined}, false, primary, undefined, identity, integer}
end;
_False -> {Field, {varchar, undefined}, false, primary, undefined, undefined, binary}
end;
get_default_user_properties(_TableType, Field, Index) when Index > 1 ->
case lists:suffix("id", atom_to_list(Field)) of
true -> {Field, {integer, undefined}, true, undefined, undefined, undefined, integer};
_False -> {Field, {varchar, undefined}, true, undefined, undefined, undefined, binary}
end.
%% @doc Return the first field of the given table if it is an identity field (auto-incrementing)
%% or return undefined
get_identity_field(Table) ->
[Field | _Rest] = table_fields(Table),
case get_user_properties(Table, Field) of
{Field, {_Type, _Modifier}, _Null, _Key, _Default, identity, _MnesiaType} -> Field;
_Other -> undefined
end.
%% @doc Find the field's position in the given table
field_index(Table, Field) ->
field_index(Field, 1, table_fields(Table)).
field_index(Field, Index, [Field | _Rest]) ->
Index;
field_index(Field, Index, [_Field | Rest]) ->
field_index(Field, Index + 1, Rest);
field_index(_Field, _Index, []) ->
0.
table_type(Table) ->
mnesia:table_info(Table, type).
table_fields(Table) ->
mnesia:table_info(Table, attributes).
table_size(Table) ->
mnesia:table_info(Table, size).
mnesia_type(Table, Field) ->
{Field, {_Type, _Modifier}, _Null, _Key, _Default, _Extra, MnesiaType} =
get_user_properties(Table, Field),
MnesiaType.
%% Convert Value to the type of the given Table Field. No conversion takes place if there is
%% no defined type for Field.
convert(Table, Field, Value) ->
convert(Value, mnesia_type(Table, Field)).
% FIXME there has to be some utility out there to do this conversion stuff...
convert(undefined, _Type) ->
undefined;
convert(Value, undefined) ->
Value;
convert(Value, integer) when is_integer(Value) ->
Value;
convert(Value, float) when is_integer(Value) ->
Value;
convert(Value, list) when is_integer(Value) ->
integer_to_list(Value);
convert(Value, atom) when is_integer(Value) ->
list_to_atom(integer_to_list(Value));
convert(Value, binary) when is_integer(Value) ->
list_to_binary(integer_to_list(Value));
convert(Value, integer) when is_float(Value) ->
trunc(Value);
convert(Value, float) when is_float(Value) ->
Value;
convert(Value, list) when is_float(Value) ->
float_to_list(Value);
convert(Value, atom) when is_float(Value) ->
list_to_atom(float_to_list(Value));
convert(Value, binary) when is_float(Value) ->
list_to_binary(float_to_list(Value));
convert(Value, integer) when is_list(Value) ->
list_to_integer(Value);
convert(Value, float) when is_list(Value) ->
list_to_float(Value);
convert(Value, list) when is_list(Value) ->
Value;
convert(Value, atom) when is_list(Value) ->
list_to_atom(Value);
convert(Value, binary) when is_list(Value) ->
list_to_binary(Value);
convert(Value, integer) when is_atom(Value) ->
list_to_integer(atom_to_list(Value));
convert(Value, float) when is_atom(Value) ->
list_to_float(atom_to_list(Value));
convert(Value, list) when is_atom(Value) ->
atom_to_list(Value);
convert(Value, atom) when is_atom(Value) ->
Value;
convert(Value, binary) when is_atom(Value) ->
list_to_binary(atom_to_list(Value));
convert(Value, integer) when is_binary(Value) ->
list_to_integer(binary_to_list(Value));
convert(Value, float) when is_binary(Value) ->
list_to_float(binary_to_list(Value));
convert(Value, list) when is_binary(Value) ->
binary_to_list(Value);
convert(Value, atom) when is_binary(Value) ->
list_to_atom(binary_to_list(Value));
convert(Value, binary) when is_binary(Value) ->
Value;
convert(Value, binary) ->
% catch all
Value;
convert({datetime, Value}, datetime) ->
Value;
convert(Value, datetime) ->
Value;
convert({date, Value}, date) ->
Value;
convert(Value, date) ->
Value;
convert({time, Value}, time) ->
Value;
convert(Value, time) ->
Value.
resolve_field(From, QLCData) ->
resolve_field(From, dict:fetch(tables, QLCData), QLCData).
resolve_field(From, Tables, QLCData) when is_list(From) ->
resolve_field(list_to_atom(From), Tables, QLCData);
resolve_field(From, Tables, QLCData) ->
lists:foldl(
fun(Table, Acc) -> case dict:is_key({Table,From}, QLCData) of true -> [Table | Acc]; _ -> Acc end end,
[],
Tables).
write(Record, Field, Value, QLCData) when is_list(Field) == false ->
write(Record, [Field], [Value], QLCData);
write(Record, [Field | Fields], [Value | Values], QLCData) ->
Table = element(1, Record),
FieldIndex = dict:fetch({index, Table, Field}, QLCData),
Record1 = setelement(FieldIndex, Record, convert(Table, Field, Value)),
write(Record1, Fields, Values, QLCData);
write(Record, [], [], _QLCData) ->
ok = mnesia:write(Record).
%% @doc Traverse the table executing the given function with each record. The function must
%% except the record followed by the given arguments and return its arguments which will be
%% supplied in the next call. For example: sum(Record, Num) -> Num + 1.
traverse(Fun, Args, Table) ->
mnesia:transaction(fun() -> traverse(Fun, Args, Table, mnesia:first(Table)) end).
traverse(_Fun, Args, _Table, '$end_of_table') ->
{ok, Args};
traverse(Fun, Args, Table, Key) ->
% for set and ordered_set tables this will execute once, for bag tables this could execute many times...
Args2 = lists:foldl(fun(Record, Args1) -> Fun(Record, Args1) end, Args, mnesia:read(Table, Key, write)),
traverse(Fun, Args2, Table, mnesia:next(Table, Key)).
comma(List) ->
combinewith(", ", List).
combinewith(Separator, List) ->
Length = length(List),
lists:foldl(
fun(Elem, {Len, String}) when Len < Length -> {Len + 1, lists:concat([String, Elem, Separator])};
(Elem, {Len, String}) when Len == Length -> lists:concat([String, Elem]) end, {1, ""}, List).
%% @doc Execute a group of statements in a transaction.
%% Fun is the function that implements the transaction.
%% Fun can contain an arbitrary sequence of calls to
%% the erlydb_mnesia's query functions. If Fun crashes or returns
%% or throws 'error' or {error, Err}, the transaction is automatically
%% rolled back.
%%
%% @spec transaction(Fun::function(), Options::options()) ->
%% {atomic, Result} | {aborted, Reason}
transaction(Fun, _Options) ->
mnesia:transaction(Fun).
%% @doc Execute a statement against Mnesia.
%%
%% @spec select(Statement::statement(), Options::options()) ->
%% {ok, Rows::list()} | {error, Error}
select(Statement, Options) ->
select2(Statement, Options, []).
%% @doc Execute a statement for records belonging to the given module,
%% returning all rows with additional data to support
%% higher-level ErlyDB features.
%%
%% @spec select_as(Module::atom(), Statement::statement(), Options::options()) ->
%% {ok, Rows} | {error, Error}
select_as(Module, Statement, Options) ->
select2(Statement, Options, [Module, false]).
select2(Statement, Options, FixedVals) ->
get_select_result(q(Statement, Options), FixedVals).
get_select_result({data, Data}, undefined) ->
Result = lists:foldl(fun(DataTuple, Acc) -> [tuple_to_list(DataTuple) | Acc] end, [], Data),
{ok, lists:reverse(Result)};
get_select_result({data, Data}, [Table | _Rest] = FixedVals)->
Results = lists:foldl(
fun(DataTuple, Acc) ->
% some data tuples are the records themselves with the table/record name as the first element...
[Table2 | Fields] = DataList = tuple_to_list(DataTuple),
Row = if Table == Table2 -> FixedVals ++ Fields;
true -> FixedVals ++ DataList
end,
[list_to_tuple(Row) | Acc]
end, [], Data),
{ok, lists:reverse(Results)};
get_select_result(Other, _) -> Other.
%% @doc Execute a update to Mnesia.
%%
%% @spec update(Statement::statement(), Options::options()) ->
%% {ok, NumAffected} | {error, Err}
update(Statement, Options) ->
q(Statement, Options).
%% @doc Get the id of the last inserted record.
%%
%% @spec get_last_insert_id(Table::atom(), Options::options()) -> term()
get_last_insert_id(_Table, _Options) ->
Val = get(mnesia_last_insert_id),
{ok, Val}.