<commit_before>%% @doc Chicago Boss database abstraction

-module(boss_db).

-export([start/1, stop/0]).

-export([
        find/1, 
        find/2, 
        find/3, 
        find_first/2,
        find_first/3,
        find_last/2,
        find_last/3,
        get/1,
        count/1,
        count/2,
        counter/1, 
        incr/1, 
        incr/2, 
        delete/1, 
        save_record/1, 
        push/0,
        pop/0,
	create_table/2,
        depth/0,
        dump/0,
        execute/1,
        execute/2,
        transaction/1,
        validate_record/1,
        validate_record_types/1,
        type/1,
        data_type/2]).

-define(DEFAULT_TIMEOUT, (30 * 1000)).
-define(POOLNAME, boss_db_pool).

start(Options) ->
    AdapterName = proplists:get_value(adapter, Options, mock),
    Adapter = list_to_atom(lists:concat(["boss_db_adapter_", AdapterName])),
    Adapter:start(Options),
    lists:foldr(fun(ShardOptions, Acc) ->
                case proplists:get_value(db_shard_models, ShardOptions, []) of
                    [] -> Acc;
                    _ ->
                        ShardAdapter = case proplists:get_value(db_adapter, ShardOptions) of
                            undefined -> Adapter;
                            ShortName -> list_to_atom(lists:concat(["boss_db_adapter_", ShortName]))
                        end,
                        ShardAdapter:start(ShardOptions ++ Options),
                        Acc
                end
        end, [], proplists:get_value(shards, Options, [])),
    boss_db_sup:start_link(Options).

stop() ->
    ok.

db_call(Msg) ->
    case erlang:get(boss_db_transaction_info) of
        undefined ->
            boss_pool:call(?POOLNAME, Msg, ?DEFAULT_TIMEOUT);
        State ->
            {reply, Reply, _} = boss_db_controller:handle_call(Msg, self(), State),
            Reply
    end.

%% @spec find(Id::string()) -> BossRecord | {error, Reason}
%% @doc Find a BossRecord with the specified `Id'.
find("") -> undefined;
find(Key) when is_list(Key) ->
    db_call({find, Key});
find(_) ->
    {error, invalid_id}.

%% @spec get(Path::string()) -> Value | {error, Reason} | undefined
%% @doc Find a BossRecord or value described by the dot-separated `Path' (e.g., "employee-42.manager.name").
get(Path) when is_list(Path) ->
    [IdToken|Rest] = string:tokens(Path, "."),
    case find(IdToken) of
        {error, Reason} ->
            {error, Reason};
        BossRecord ->
            BossRecord:get(string:join(Rest, "."))
    end.

%% @spec find(Type::atom(), Conditions) -> [ BossRecord ]
%% @doc Query for BossRecords. Returns all BossRecords of type
%% `Type' matching all of the given `Conditions'
find(Type, Conditions) ->
    find(Type, Conditions, []).

%% @spec find(Type::atom(), Conditions, Options::proplist()) -> [ BossRecord ]
%% @doc Query for BossRecords. Returns BossRecords of type
%% `Type' matching all of the given `Conditions'. Options may include
%% `limit' (maximum number of records to return), `offset' (number of records
%% to skip), `order_by' (attribute to sort on), and `descending' (whether to
%% sort the values from highest to lowest)
find(Type, Conditions, Options) ->
    Max = proplists:get_value(limit, Options, all),
    Skip = proplists:get_value(offset, Options, 0),
    Sort = proplists:get_value(order_by, Options, id),
    SortOrder = case proplists:get_value(descending, Options) of
        true -> descending;
        _ -> ascending
    end,
    db_call({find, Type, normalize_conditions(Conditions), Max, Skip, Sort, SortOrder}).

%% @spec find_first( Type::atom(), Conditions ) -> Record | undefined
%% @doc Query for the first BossRecord of type `Type' matching all of the given `Conditions'
find_first(Type, Conditions) ->
    return_one(find(Type, Conditions, [{limit, 1}])).

%% @spec find_first( Type::atom(), Conditions, Sort::atom() ) -> Record | undefined
%% @doc Query for the first BossRecord of type `Type' matching all of the given `Conditions',
%% sorted on the attribute `Sort'.
find_first(Type, Conditions, Sort) ->
    return_one(find(Type, Conditions, [{limit, 1}, {order_by, Sort}])).

%% @spec find_last( Type::atom(), Conditions ) -> Record | undefined
%% @doc Query for the last BossRecord of type `Type' matching all of the given `Conditions'
find_last(Type, Conditions) ->
    return_one(find(Type, Conditions, [{limit, 1}, descending])).

%% @spec find_last( Type::atom(), Conditions, Sort ) -> Record | undefined
%% @doc Query for the last BossRecord of type `Type' matching all of the given `Conditions'
find_last(Type, Conditions, Sort) ->
    return_one(find(Type, Conditions, [{limit, 1}, {order_by, Sort}, descending])).

%% @spec count( Type::atom() ) -> integer()
%% @doc Count the number of BossRecords of type `Type' in the database.
count(Type) ->
    count(Type, []).

%% @spec count( Type::atom(), Conditions ) -> integer()
%% @doc Count the number of BossRecords of type `Type' in the database matching
%% all of the given `Conditions'.
count(Type, Conditions) ->
    db_call({count, Type, normalize_conditions(Conditions)}).

%% @spec counter( Id::string() ) -> integer()
%% @doc Treat the record associated with `Id' as a counter and return its value.
%% Returns 0 if the record does not exist, so to reset a counter just use
%% "delete".
counter(Key) ->
    db_call({counter, Key}).

%% @spec incr( Id::string() ) -> integer()
%% @doc Treat the record associated with `Id' as a counter and atomically increment its value by 1.
incr(Key) ->
    incr(Key, 1).

%% @spec incr( Id::string(), Increment::integer() ) -> integer()
%% @doc Treat the record associated with `Id' as a counter and atomically increment its value by `Increment'.
incr(Key, Count) ->
    db_call({incr, Key, Count}).

%% @spec delete( Id::string() ) -> ok | {error, Reason}
%% @doc Delete the BossRecord with the given `Id'.
delete(Key) ->
    case boss_db:find(Key) of
        undefined ->
            {error, not_found};
        AboutToDelete ->
            case boss_record_lib:run_before_delete_hooks(AboutToDelete) of
                ok ->
                    Result = db_call({delete, Key}),
                    case Result of
                        ok -> 
                            boss_news:deleted(Key, AboutToDelete:attributes()),
                            ok;
                        _ -> 
                            Result
                    end;
                {error, Reason} ->
                    {error, Reason}
            end
    end.

push() ->
    db_call(push).

pop() ->
    db_call(pop).

depth() ->
    db_call(depth).

dump() ->
    db_call(dump).

create_table(TableName, TableDefinition) ->
    db_call({create_table, TableName, TableDefinition}).

%% @spec execute( Commands::iolist() ) -> RetVal
%% @doc Execute raw database commands on SQL databases
execute(Commands) ->
    db_call({execute, Commands}).

%% @spec execute( Commands::iolist(), Params::list() ) -> RetVal
%% @doc Execute database commands with interpolated parameters on SQL databases
execute(Commands, Params) ->
    db_call({execute, Commands, Params}).

%% @spec transaction( TransactionFun::function() ) -> {atomic, Result} | {aborted, Reason}
%% @doc Execute a fun inside a transaction.
transaction(TransactionFun) ->
    Worker = poolboy:checkout(?POOLNAME),
    State = gen_server:call(Worker, state, ?DEFAULT_TIMEOUT),
    put(boss_db_transaction_info, State),
    {reply, Reply, _} = boss_db_controller:handle_call({transaction, TransactionFun}, self(), State),
    put(boss_db_transaction_info, undefined),
    poolboy:checkin(?POOLNAME, Worker),
    Reply.

%% @spec save_record( BossRecord ) -> {ok, SavedBossRecord} | {error, [ErrorMessages]}
%% @doc Save (that is, create or update) the given BossRecord in the database.
%% Performs validation first; see `validate_record/1'.
save_record(Record) ->
    case validate_record(Record) of
        ok ->
            RecordId = Record:id(),
            {IsNew, OldRecord} = if
                RecordId =:= 'id' ->
                    {true, Record};
                true ->
                    case find(RecordId) of
                        {error, _Reason} -> {true, Record};
                        undefined -> {true, Record};
                        FoundOldRecord -> {false, FoundOldRecord}
                    end
            end,
            HookResult = case boss_record_lib:run_before_hooks(Record, IsNew) of
                ok -> {ok, Record};
                {ok, Record1} -> {ok, Record1};
                {error, Reason} -> {error, Reason}
            end,
            case HookResult of
                {ok, PossiblyModifiedRecord} ->
                    case db_call({save_record, PossiblyModifiedRecord}) of
                        {ok, SavedRecord} ->
                            boss_record_lib:run_after_hooks(OldRecord, SavedRecord, IsNew),
                            {ok, SavedRecord};
                        Err -> Err
                    end;
                Err -> Err
            end;
        Err -> Err
    end.

%% @spec validate_record( BossRecord ) -> ok | {error, [ErrorMessages]}
%% @doc Validate the given BossRecord without saving it in the database.
%% `ErrorMessages' are generated from the list of tests returned by the BossRecord's 
%% `validation_tests/0' function (if defined). The returned list should consist of
%% `{TestFunction, ErrorMessage}' tuples, where `TestFunction' is a fun of arity 0 
%% that returns `true' if the record is valid or `false' if it is invalid. 
%% `ErrorMessage' should be a (constant) string which will be included in `ErrorMessages'
%% if the `TestFunction' returns `false' on this particular BossRecord.
validate_record(Record) ->
    Type = element(1, Record),
    Errors1 = case validate_record_types(Record) of
        ok -> [];
        {error, Errors} -> Errors
    end,
    Errors2 = case Errors1 of
        [] ->
            case erlang:function_exported(Type, validation_tests, 1) of
                true -> [String || {TestFun, String} <- Record:validation_tests(), not TestFun()];
                false -> []
            end;
        _ -> Errors1
    end,
    case length(Errors2) of
        0 -> ok;
        _ -> {error, Errors2}
    end.

%% @spec validate_record_types( BossRecord ) -> ok | {error, [ErrorMessages]}
%% @doc Validate the parameter types of the given BossRecord without saving it
%% to the database.
validate_record_types(Record) ->
    Errors = lists:foldl(fun
            ({Attr, Type}, Acc) ->
                case Attr of
                  id -> Acc;
                  _  ->                                 
                    Data = Record:Attr(),
                    GreatSuccess = case {Data, Type} of
                        {undefined, _} ->
                            true;
                        {Data, string} when is_list(Data) ->
                            true;
                        {Data, binary} when is_binary(Data) ->
                            true;
                        {{{D1, D2, D3}, {T1, T2, T3}}, datetime} when is_integer(D1), is_integer(D2), is_integer(D3), 
                                                                      is_integer(T1), is_integer(T2), is_integer(T3) ->
                            true;
                        {{D1, D2, D3}, date} when is_integer(D1), is_integer(D2), is_integer(D3) ->
                            true;
                        {Data, integer} when is_integer(Data) ->
                            true;
                        {Data, float} when is_float(Data) ->
                            true;
                        {Data, boolean} when is_boolean(Data) ->
                            true;
                        {{N1, N2, N3}, timestamp} when is_integer(N1), is_integer(N2), is_integer(N3) ->
                            true;
                        {Data, atom} when is_atom(Data) ->
                            true;
                        {_Data, Type} ->
                            false
                    end,
                    if
                        GreatSuccess ->
                            Acc;
                        true -> 
                            [lists:concat(["Invalid data type for ", Attr])|Acc]
                    end
                  end
        end, [], Record:attribute_types()),
    case Errors of
        [] -> ok;
        _ -> {error, Errors}
    end.

%% @spec type( Id::string() ) -> Type::atom()
%% @doc Returns the type of the BossRecord with `Id', or `undefined' if the record does not exist.
type(Key) ->
    case find(Key) of
        undefined -> undefined;
        Record -> element(1, Record)
    end.

data_type(_, _Val) when is_float(_Val) ->
    "float";
data_type(_, _Val) when is_binary(_Val) ->
    "binary";
data_type(_, _Val) when is_integer(_Val) ->
    "integer";
data_type(_, _Val) when is_tuple(_Val) ->
    "datetime";
data_type(_, _Val) when is_boolean(_Val) ->
    "boolean";
data_type(_, null) ->
    "null";
data_type(_, undefined) ->
    "null";
data_type('id', _) ->
    "id";
data_type(Key, Val) when is_list(Val) ->
    case lists:suffix("_id", atom_to_list(Key)) of
        true -> "foreign_id";
        false -> "string"
    end.

normalize_conditions(Conditions) ->
    normalize_conditions(Conditions, []).

normalize_conditions([], Acc) ->
    lists:reverse(Acc);
normalize_conditions([Key, Operator, Value|Rest], Acc) when is_atom(Key), is_atom(Operator) ->
    normalize_conditions(Rest, [{Key, Operator, Value}|Acc]);
normalize_conditions([{Key, Value}|Rest], Acc) when is_atom(Key) ->
    normalize_conditions(Rest, [{Key, 'equals', Value}|Acc]);
normalize_conditions([{Key, Operator, Value}|Rest], Acc) when is_atom(Key), is_atom(Operator) ->
    normalize_conditions(Rest, [{Key, Operator, Value}|Acc]).

return_one(Result) ->
    case Result of
        [] -> undefined;
        [Record] -> Record
    end.
<commit_msg>Added migrate and table_exist functions.<commit_after>%% @doc Chicago Boss database abstraction

-module(boss_db).

-export([start/1, stop/0]).

-export([
	 migrate/1,
        find/1, 
        find/2, 
        find/3, 
        find_first/2,
        find_first/3,
        find_last/2,
        find_last/3,
        get/1,
        count/1,
        count/2,
        counter/1, 
        incr/1, 
        incr/2, 
        delete/1, 
        save_record/1, 
        push/0,
        pop/0,
	create_table/2,
	table_exists/1,
        depth/0,
        dump/0,
        execute/1,
        execute/2,
        transaction/1,
        validate_record/1,
        validate_record_types/1,
        type/1,
        data_type/2]).

-define(DEFAULT_TIMEOUT, (30 * 1000)).
-define(POOLNAME, boss_db_pool).

start(Options) ->
    AdapterName = proplists:get_value(adapter, Options, mock),
    Adapter = list_to_atom(lists:concat(["boss_db_adapter_", AdapterName])),
    Adapter:start(Options),
    lists:foldr(fun(ShardOptions, Acc) ->
                case proplists:get_value(db_shard_models, ShardOptions, []) of
                    [] -> Acc;
                    _ ->
                        ShardAdapter = case proplists:get_value(db_adapter, ShardOptions) of
                            undefined -> Adapter;
                            ShortName -> list_to_atom(lists:concat(["boss_db_adapter_", ShortName]))
                        end,
                        ShardAdapter:start(ShardOptions ++ Options),
                        Acc
                end
        end, [], proplists:get_value(shards, Options, [])),
    boss_db_sup:start_link(Options).

stop() ->
    ok.

db_call(Msg) ->
    case erlang:get(boss_db_transaction_info) of
        undefined ->
            boss_pool:call(?POOLNAME, Msg, ?DEFAULT_TIMEOUT);
        State ->
            {reply, Reply, _} = boss_db_controller:handle_call(Msg, self(), State),
            Reply
    end.

migrate(Migrations) when is_list(Migrations) ->
    %% 1. Do we have a migrations table?  If not, create it.
    case table_exists(schema_migrations) of
	false ->
	    ok = create_table(schema_migrations, [{id, auto_increment, []},
						    {version, string, [not_null]},
						    {migrated_at, datetime, []}]);
	_ ->
	    noop
    end,
    %% 2. Get all the current migrations from it.
    DoneMigrations = db_call({get_migrations_table}),
    DoneMigrationTags = [list_to_atom(binary_to_list(Tag)) || {_Id, Tag, _MigratedAt} <- DoneMigrations],
    %% 3. Run the ones that are not in this list.
    transaction(fun() ->
			[migrate({Tag, Fun}, up) || {Tag, Fun} <- Migrations, not lists:member(Tag, DoneMigrationTags)]
		end).

migrate({_Tag, Fun}, Direction) ->
    Fun(Direction).


%% @spec find(Id::string()) -> BossRecord | {error, Reason}
%% @doc Find a BossRecord with the specified `Id'.
find("") -> undefined;
find(Key) when is_list(Key) ->
    db_call({find, Key});
find(_) ->
    {error, invalid_id}.

%% @spec get(Path::string()) -> Value | {error, Reason} | undefined
%% @doc Find a BossRecord or value described by the dot-separated `Path' (e.g., "employee-42.manager.name").
get(Path) when is_list(Path) ->
    [IdToken|Rest] = string:tokens(Path, "."),
    case find(IdToken) of
        {error, Reason} ->
            {error, Reason};
        BossRecord ->
            BossRecord:get(string:join(Rest, "."))
    end.

%% @spec find(Type::atom(), Conditions) -> [ BossRecord ]
%% @doc Query for BossRecords. Returns all BossRecords of type
%% `Type' matching all of the given `Conditions'
find(Type, Conditions) ->
    find(Type, Conditions, []).

%% @spec find(Type::atom(), Conditions, Options::proplist()) -> [ BossRecord ]
%% @doc Query for BossRecords. Returns BossRecords of type
%% `Type' matching all of the given `Conditions'. Options may include
%% `limit' (maximum number of records to return), `offset' (number of records
%% to skip), `order_by' (attribute to sort on), and `descending' (whether to
%% sort the values from highest to lowest)
find(Type, Conditions, Options) ->
    Max = proplists:get_value(limit, Options, all),
    Skip = proplists:get_value(offset, Options, 0),
    Sort = proplists:get_value(order_by, Options, id),
    SortOrder = case proplists:get_value(descending, Options) of
        true -> descending;
        _ -> ascending
    end,
    db_call({find, Type, normalize_conditions(Conditions), Max, Skip, Sort, SortOrder}).

%% @spec find_first( Type::atom(), Conditions ) -> Record | undefined
%% @doc Query for the first BossRecord of type `Type' matching all of the given `Conditions'
find_first(Type, Conditions) ->
    return_one(find(Type, Conditions, [{limit, 1}])).

%% @spec find_first( Type::atom(), Conditions, Sort::atom() ) -> Record | undefined
%% @doc Query for the first BossRecord of type `Type' matching all of the given `Conditions',
%% sorted on the attribute `Sort'.
find_first(Type, Conditions, Sort) ->
    return_one(find(Type, Conditions, [{limit, 1}, {order_by, Sort}])).

%% @spec find_last( Type::atom(), Conditions ) -> Record | undefined
%% @doc Query for the last BossRecord of type `Type' matching all of the given `Conditions'
find_last(Type, Conditions) ->
    return_one(find(Type, Conditions, [{limit, 1}, descending])).

%% @spec find_last( Type::atom(), Conditions, Sort ) -> Record | undefined
%% @doc Query for the last BossRecord of type `Type' matching all of the given `Conditions'
find_last(Type, Conditions, Sort) ->
    return_one(find(Type, Conditions, [{limit, 1}, {order_by, Sort}, descending])).

%% @spec count( Type::atom() ) -> integer()
%% @doc Count the number of BossRecords of type `Type' in the database.
count(Type) ->
    count(Type, []).

%% @spec count( Type::atom(), Conditions ) -> integer()
%% @doc Count the number of BossRecords of type `Type' in the database matching
%% all of the given `Conditions'.
count(Type, Conditions) ->
    db_call({count, Type, normalize_conditions(Conditions)}).

%% @spec counter( Id::string() ) -> integer()
%% @doc Treat the record associated with `Id' as a counter and return its value.
%% Returns 0 if the record does not exist, so to reset a counter just use
%% "delete".
counter(Key) ->
    db_call({counter, Key}).

%% @spec incr( Id::string() ) -> integer()
%% @doc Treat the record associated with `Id' as a counter and atomically increment its value by 1.
incr(Key) ->
    incr(Key, 1).

%% @spec incr( Id::string(), Increment::integer() ) -> integer()
%% @doc Treat the record associated with `Id' as a counter and atomically increment its value by `Increment'.
incr(Key, Count) ->
    db_call({incr, Key, Count}).

%% @spec delete( Id::string() ) -> ok | {error, Reason}
%% @doc Delete the BossRecord with the given `Id'.
delete(Key) ->
    case boss_db:find(Key) of
        undefined ->
            {error, not_found};
        AboutToDelete ->
            case boss_record_lib:run_before_delete_hooks(AboutToDelete) of
                ok ->
                    Result = db_call({delete, Key}),
                    case Result of
                        ok -> 
                            boss_news:deleted(Key, AboutToDelete:attributes()),
                            ok;
                        _ -> 
                            Result
                    end;
                {error, Reason} ->
                    {error, Reason}
            end
    end.

push() ->
    db_call(push).

pop() ->
    db_call(pop).

depth() ->
    db_call(depth).

dump() ->
    db_call(dump).

%% @spec create_table ( TableName::string(), TableDefinition ) -> ok | {error, Reason}
%% @doc Create a table based on TableDefinition
create_table(TableName, TableDefinition) ->
    db_call({create_table, TableName, TableDefinition}).

table_exists(TableName) ->
    db_call({table_exists, TableName}).

%% @spec execute( Commands::iolist() ) -> RetVal
%% @doc Execute raw database commands on SQL databases
execute(Commands) ->
    db_call({execute, Commands}).

%% @spec execute( Commands::iolist(), Params::list() ) -> RetVal
%% @doc Execute database commands with interpolated parameters on SQL databases
execute(Commands, Params) ->
    db_call({execute, Commands, Params}).

%% @spec transaction( TransactionFun::function() ) -> {atomic, Result} | {aborted, Reason}
%% @doc Execute a fun inside a transaction.
transaction(TransactionFun) ->
    Worker = poolboy:checkout(?POOLNAME),
    State = gen_server:call(Worker, state, ?DEFAULT_TIMEOUT),
    put(boss_db_transaction_info, State),
    {reply, Reply, _} = boss_db_controller:handle_call({transaction, TransactionFun}, self(), State),
    put(boss_db_transaction_info, undefined),
    poolboy:checkin(?POOLNAME, Worker),
    Reply.

%% @spec save_record( BossRecord ) -> {ok, SavedBossRecord} | {error, [ErrorMessages]}
%% @doc Save (that is, create or update) the given BossRecord in the database.
%% Performs validation first; see `validate_record/1'.
save_record(Record) ->
    case validate_record(Record) of
        ok ->
            RecordId = Record:id(),
            {IsNew, OldRecord} = if
                RecordId =:= 'id' ->
                    {true, Record};
                true ->
                    case find(RecordId) of
                        {error, _Reason} -> {true, Record};
                        undefined -> {true, Record};
                        FoundOldRecord -> {false, FoundOldRecord}
                    end
            end,
            HookResult = case boss_record_lib:run_before_hooks(Record, IsNew) of
                ok -> {ok, Record};
                {ok, Record1} -> {ok, Record1};
                {error, Reason} -> {error, Reason}
            end,
            case HookResult of
                {ok, PossiblyModifiedRecord} ->
                    case db_call({save_record, PossiblyModifiedRecord}) of
                        {ok, SavedRecord} ->
                            boss_record_lib:run_after_hooks(OldRecord, SavedRecord, IsNew),
                            {ok, SavedRecord};
                        Err -> Err
                    end;
                Err -> Err
            end;
        Err -> Err
    end.

%% @spec validate_record( BossRecord ) -> ok | {error, [ErrorMessages]}
%% @doc Validate the given BossRecord without saving it in the database.
%% `ErrorMessages' are generated from the list of tests returned by the BossRecord's 
%% `validation_tests/0' function (if defined). The returned list should consist of
%% `{TestFunction, ErrorMessage}' tuples, where `TestFunction' is a fun of arity 0 
%% that returns `true' if the record is valid or `false' if it is invalid. 
%% `ErrorMessage' should be a (constant) string which will be included in `ErrorMessages'
%% if the `TestFunction' returns `false' on this particular BossRecord.
validate_record(Record) ->
    Type = element(1, Record),
    Errors1 = case validate_record_types(Record) of
        ok -> [];
        {error, Errors} -> Errors
    end,
    Errors2 = case Errors1 of
        [] ->
            case erlang:function_exported(Type, validation_tests, 1) of
                true -> [String || {TestFun, String} <- Record:validation_tests(), not TestFun()];
                false -> []
            end;
        _ -> Errors1
    end,
    case length(Errors2) of
        0 -> ok;
        _ -> {error, Errors2}
    end.

%% @spec validate_record_types( BossRecord ) -> ok | {error, [ErrorMessages]}
%% @doc Validate the parameter types of the given BossRecord without saving it
%% to the database.
validate_record_types(Record) ->
    Errors = lists:foldl(fun
            ({Attr, Type}, Acc) ->
                case Attr of
                  id -> Acc;
                  _  ->                                 
                    Data = Record:Attr(),
                    GreatSuccess = case {Data, Type} of
                        {undefined, _} ->
                            true;
                        {Data, string} when is_list(Data) ->
                            true;
                        {Data, binary} when is_binary(Data) ->
                            true;
                        {{{D1, D2, D3}, {T1, T2, T3}}, datetime} when is_integer(D1), is_integer(D2), is_integer(D3), 
                                                                      is_integer(T1), is_integer(T2), is_integer(T3) ->
                            true;
                        {{D1, D2, D3}, date} when is_integer(D1), is_integer(D2), is_integer(D3) ->
                            true;
                        {Data, integer} when is_integer(Data) ->
                            true;
                        {Data, float} when is_float(Data) ->
                            true;
                        {Data, boolean} when is_boolean(Data) ->
                            true;
                        {{N1, N2, N3}, timestamp} when is_integer(N1), is_integer(N2), is_integer(N3) ->
                            true;
                        {Data, atom} when is_atom(Data) ->
                            true;
                        {_Data, Type} ->
                            false
                    end,
                    if
                        GreatSuccess ->
                            Acc;
                        true -> 
                            [lists:concat(["Invalid data type for ", Attr])|Acc]
                    end
                  end
        end, [], Record:attribute_types()),
    case Errors of
        [] -> ok;
        _ -> {error, Errors}
    end.

%% @spec type( Id::string() ) -> Type::atom()
%% @doc Returns the type of the BossRecord with `Id', or `undefined' if the record does not exist.
type(Key) ->
    case find(Key) of
        undefined -> undefined;
        Record -> element(1, Record)
    end.

data_type(_, _Val) when is_float(_Val) ->
    "float";
data_type(_, _Val) when is_binary(_Val) ->
    "binary";
data_type(_, _Val) when is_integer(_Val) ->
    "integer";
data_type(_, _Val) when is_tuple(_Val) ->
    "datetime";
data_type(_, _Val) when is_boolean(_Val) ->
    "boolean";
data_type(_, null) ->
    "null";
data_type(_, undefined) ->
    "null";
data_type('id', _) ->
    "id";
data_type(Key, Val) when is_list(Val) ->
    case lists:suffix("_id", atom_to_list(Key)) of
        true -> "foreign_id";
        false -> "string"
    end.

normalize_conditions(Conditions) ->
    normalize_conditions(Conditions, []).

normalize_conditions([], Acc) ->
    lists:reverse(Acc);
normalize_conditions([Key, Operator, Value|Rest], Acc) when is_atom(Key), is_atom(Operator) ->
    normalize_conditions(Rest, [{Key, Operator, Value}|Acc]);
normalize_conditions([{Key, Value}|Rest], Acc) when is_atom(Key) ->
    normalize_conditions(Rest, [{Key, 'equals', Value}|Acc]);
normalize_conditions([{Key, Operator, Value}|Rest], Acc) when is_atom(Key), is_atom(Operator) ->
    normalize_conditions(Rest, [{Key, Operator, Value}|Acc]).

return_one(Result) ->
    case Result of
        [] -> undefined;
        [Record] -> Record
    end.

{'fraction_non_alphanumeric': 0.1307620269848427, 'fraction_numerical': 0.006000485588429121, 'mean_word_length': 2.1582867783985105, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 270, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '179800', 'n_tokens_mistral': 8625, 'n_tokens_neox': 8262, 'n_words': 2321}