diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml new file mode 100644 index 00000000..dc4d4276 --- /dev/null +++ b/.github/workflows/workflow.yml @@ -0,0 +1,24 @@ +name: Build and test + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-20.04 + name: Erlang/OTP ${{matrix.otp}} / rebar3 ${{matrix.rebar3}} + strategy: + fail-fast: false + matrix: + otp: ['23.0.2', '24.1.3', '25.1.1'] + rebar3: ['3.20.0'] + steps: + - uses: actions/checkout@v2 + - uses: erlef/setup-beam@v1 + with: + otp-version: ${{matrix.otp}} + rebar3-version: ${{matrix.rebar3}} + version-type: strict + - name: Compile + run: rebar3 compile + - name: Run tests + run: rebar3 eunit diff --git a/.gitignore b/.gitignore index 9af73229..2ef6fb04 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,6 @@ rebar3.crashdump .eunit /ebin /doc -rebar.lock .project .idea +rebar.lock diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 79fa3c49..00000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: erlang -sudo: false -otp_release: -# Test on all supported releases & some more - - 23.1.2 - - 22.3 - - 21.3 - - 20.3 - - 19.3 - -script: - - make - - make test - - make dialyze - -notifications: - email: false diff --git a/Makefile b/Makefile deleted file mode 100644 index 81ecaeb7..00000000 --- a/Makefile +++ /dev/null @@ -1,56 +0,0 @@ -ERL=erl -REBAR=rebar3 -GIT = git -REBAR_VER = 3.6.2 -DB_CONFIG_DIR=priv/test_db_config - -.PHONY: test - -all: compile - -compile: - @$(REBAR) compile - -rebar_src: - @rm -rf $(PWD)/rebar_src - @$(GIT) clone https://github.com/erlang/rebar3.git rebar_src - @$(GIT) -C rebar_src checkout tags/$(REBAR_VER) - @cd $(PWD)/rebar_src/; ./bootstrap - @cp $(PWD)/rebar_src/rebar3 $(PWD) - @rm -rf $(PWD)/rebar_src - -get-deps: - @$(REBAR) upgrade - -deps: - @$(REBAR) compile - -.PHONY: dialyze -dialyze: - @$(REBAR) dialyzer || [ $$? -eq 1 ]; - -clean: - @$(REBAR) clean - rm -fv erl_crash.dump - -test: - @$(REBAR) eunit - -compile_db_test: - @$(REBAR) as test, boss_test do clean, compile - -test_db_mock: compile_db_test - $(ERL) -pa _build/test+boss_test/lib/*/ebin -run boss_db_test start -config $(DB_CONFIG_DIR)/mock -noshell - -test_db_mysql: compile_db_test - $(ERL) -pa _build/test+boss_test/lib/*/ebin -run boss_db_test start -config $(DB_CONFIG_DIR)/mysql -noshell - -test_db_pgsql: compile_db_test - $(ERL) -pa _build/test+boss_test/lib/*/ebin -run boss_db_test start -config $(DB_CONFIG_DIR)/pgsql -noshell - -test_db_mongodb: compile_db_test - echo "db.boss_db_test_models.remove();"|mongo boss_test - $(ERL) -pa _build/test+boss_test/lib/*/ebin -run boss_db_test start -config $(DB_CONFIG_DIR)/mongodb -noshell - -test_db_riak: compile_db_test - $(ERL) -pa _build/test+boss_test/lib/*/ebin -run boss_db_test start -config $(DB_CONFIG_DIR)/riak -noshell diff --git a/README.md b/README.md index ebd2e8ce..3a9f92a6 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,18 @@ BossDB: A sharded, caching, pooling, evented ORM for Erlang =========================================================== -[![Build Status](https://travis-ci.org/ErlyORM/boss_db.svg?branch=master)](https://travis-ci.org/ErlyORM/boss_db) -Attention! This is a master branch supporting Erlang 18 and above. For older Erlang versions use legacy branch. +[![Build and test](https://github.com/burbas/boss_db/actions/workflows/workflow.yml/badge.svg?branch=master)](https://github.com/burbas/boss_db/actions/workflows/workflow.yml) + +**Important** This fork deviates from boss_db and does not support Erlang versions prior to 21. We've removed some of the database adapters since they were using very old libraries and we did not have time to port them. Supported databases ------------------- -* *NEW* DynamoDB (experimental) +* DynamoDB (experimental) * Mnesia -* MongoDB * MySQL * PostgreSQL * Riak -* Tokyo Tyrant Complete API references ----------------------- @@ -34,7 +33,7 @@ boss_cache:start(CacheOptions), % If you want cacheing with Memcached boss_news:start() % Mandatory! Hopefully will be optional one day DBOptions = [ - {adapter, mock | tyrant | riak | mysql | pgsql | mnesia | mongodb}, + {adapter, mock | riak | mysql | pgsql | mnesia}, {db_host, HostName::string()}, {db_port, PortNumber::integer()}, {db_username, UserName::string()}, @@ -83,6 +82,8 @@ EtsCacheServerOpts = [ ] ``` + + Introduction ------------ @@ -155,8 +156,8 @@ Similarly, you could iterate over all the puppies of a particular breed: ```erlang Breed = boss_db:find("breed-47"), -lists:map(fun(Puppy) -> - io:format("Puppy: ~p~n", [Puppy:name()]) +lists:map(fun(Puppy) -> + io:format("Puppy: ~p~n", [Puppy:name()]) end, Breed:puppies()) ``` @@ -291,7 +292,7 @@ BossNews is suited to providing real-time notifications and alerts. For example, if you want to log each time a puppy's name is changed, ```erlang -boss_news:watch("puppy-*.name", +boss_news:watch("puppy-*.name", fun(updated, {Puppy, 'name', OldName, NewName}) -> error_logger:info_msg("Puppy's name changed from ~p to ~p", [OldName, NewName]) end) @@ -339,3 +340,18 @@ are useful PKs when data are being aggregated from multiple sources. The default Id type ::serial() may be explicitly supplied. Note that all Id types, valid or otherwise, pass type validation. + + +Test +---- + +To test mysql adapter, you need provide mysql connection env on test. Example: + +``` +MYSQL_HOST=127.0.0.1 \ +MYSQL_PORT=3306 \ +MYSQL_USER=user \ +MYSQL_PASSWORD=test \ +MYSQL_TEST_DBNAME=test \ +rebar3 eunit --suite=boss_db_adapter_mysql_otp_test +``` \ No newline at end of file diff --git a/src/db_adapters/boss_db_adapter_types.erl b/include/boss_db.hrl similarity index 69% rename from src/db_adapters/boss_db_adapter_types.erl rename to include/boss_db.hrl index 3bac46be..d0c8c124 100644 --- a/src/db_adapters/boss_db_adapter_types.erl +++ b/include/boss_db.hrl @@ -1,4 +1,2 @@ --module(boss_db_adapter_types). -type model_operator() :: 'not_matches'|matches|contains|not_contains|contains_all|not_contains_all| contains_any| contains_none|in|not_in. --export_type([model_operator/0]). diff --git a/otp_chained_functions_r16b.patch b/otp_chained_functions_r16b.patch deleted file mode 100644 index b5b6c9c2..00000000 --- a/otp_chained_functions_r16b.patch +++ /dev/null @@ -1,20 +0,0 @@ -# Apply this patch to your Erlang/OTP sources in order to -# chain function calls like -# -# Person:new(id, "Joe"):save() -# -# To apply it, cd into your OTP source directory and type -# -# patch -p1 < /path/to/this/patch -# ---- a/lib/stdlib/src/erl_parse.yrl 2013-02-25 13:21:31.000000000 -0600 -+++ b/lib/stdlib/src/erl_parse.yrl 2013-03-26 18:54:55.000000000 -0500 -@@ -253,7 +253,7 @@ - expr_700 -> record_expr : '$1'. - expr_700 -> expr_800 : '$1'. - --expr_800 -> expr_max ':' expr_max : -+expr_800 -> expr_700 ':' expr_max : - {remote,?line('$2'),'$1','$3'}. - expr_800 -> expr_max : '$1'. - diff --git a/priv/rebar/boss_db_rebar.erl b/priv/rebar/boss_db_rebar.erl deleted file mode 100644 index b7404627..00000000 --- a/priv/rebar/boss_db_rebar.erl +++ /dev/null @@ -1,161 +0,0 @@ -%%% @author Roman Tsisyk -%%% @doc -%%% Rebar plugin to compile boss_db models -%%% -%%% Configuration options should be placed in rebar.config under -%%% 'boss_db_opts'. -%%% -%%% Available options include: -%%% -%%% model_dir: where to find templates to compile -%%% "src/model" by default -%%% -%%% out_dir: where to put compiled template beam files -%%% "ebin" by default -%%% -%%% source_ext: the file extension the template sources have -%%% ".erl" by default -%%% -%%% recursive: boolean that determines if model_dir need to be -%%% scanned recursively for matching template file names -%%% (default: false). -%%% -%%% compiler_options: options to determine the behavior of the model -%%% compiler, see the Erlang docs (compile:file/2) -%%% for valid options -%%% -%%% The default settings are the equivalent of: -%%% {boss_db_opts, [ -%%% {model_root, "src/model"}, -%%% {out_root, "ebin"}, -%%% {source_ext, ".erl"}, -%%% {recursive, false}, -%%% {compiler_options, [verbose, return_errors]} -%%% ]}. -%%% @end - --module(boss_db_rebar). - --export([pre_compile/2]). --export([pre_eunit/2]). - -%% @doc A pre-compile hook to compile boss_db models -pre_compile(RebarConf, _AppFile) -> - BossDbOpts = boss_db_opts(RebarConf), - pre_compile_helper(RebarConf, BossDbOpts, option(out_dir, BossDbOpts)). - -%% @doc A pre-eunit hook to compile boss_db models before eunit. -%% It also copies model's .erl files to the .eunit directory if -%% coverage is needed. -pre_eunit(RebarConf, _AppFile) -> - BossDbOpts = boss_db_opts(RebarConf), - pre_compile_helper(RebarConf, BossDbOpts, ".eunit"), - case coverage_modifications_needed(RebarConf, BossDbOpts) of - true -> - ModelDir = option(model_dir, BossDbOpts), - eunit_coverage_helper(ModelDir); - false -> - ok - end. - -%% -------------------------------------------------------------------- -%% Internal functions -%% -------------------------------------------------------------------- - -%% @doc Gets the boss_db options -boss_db_opts(RebarConf) -> - rebar_config:get(RebarConf, boss_db_opts, []). - -%% @doc Gets the value for coverage_enabled from the rebar config -coverage_enabled(RebarConf) -> - rebar_config:get(RebarConf, cover_enabled, false). - -%% @doc A pre-compile hook to compile boss_db models -pre_compile_helper(RebarConf, BossDbOpts, TargetDir) -> - SourceDir = option(model_dir, BossDbOpts), - SourceExt = option(source_ext, BossDbOpts), - TargetExt = ".beam", - rebar_base_compiler:run(RebarConf, [], - SourceDir, - SourceExt, - TargetDir, - TargetExt, - fun(S, T, _C) -> - compile_model(S, T, BossDbOpts, RebarConf) - end, - [{check_last_mod, true}, - {recursive, option(recursive, BossDbOpts)}]). - -option(Opt, BossDbOpts) -> - proplists:get_value(Opt, BossDbOpts, option_default(Opt)). - -option_default(model_dir) -> "src/model"; -option_default(out_dir) -> "ebin"; -option_default(source_ext) -> ".erl"; -option_default(recursive) -> false; -option_default(compiler_options) -> [verbose, return_errors]. - -compiler_options(ErlOpts, BossDbOpts) -> - set_debug_info_option(proplists:get_value(debug_info, ErlOpts), option(compiler_options, BossDbOpts)). - -set_debug_info_option(true, BossCompilerOptions) -> - [debug_info | BossCompilerOptions]; -set_debug_info_option(undefined, BossCompilerOptions) -> - BossCompilerOptions. - -compile_model(Source, Target, BossDbOpts, RebarConfig) -> - ErlOpts = rebar_config:get(RebarConfig, erl_opts, []), - RecordCompilerOpts = [{out_dir, filename:dirname(Target)}, {compiler_options, compiler_options(ErlOpts, BossDbOpts)}], - case boss_record_compiler:compile(Source, RecordCompilerOpts) of - {ok, _Mod} -> - ok; - {ok, _Mod, Ws} -> - rebar_base_compiler:ok_tuple(RebarConfig, Source, Ws); - {error, Es, Ws} -> - rebar_base_compiler:error_tuple(RebarConfig, Source, - Es, Ws, RecordCompilerOpts) - end. - -%% @private -%% @doc copies `.erl' files into the `.eunit' directory -eunit_coverage_helper(ModelDir) -> - {ok, Filenames} = file:list_dir(ModelDir), - lists:foreach( - fun(Filename) -> - copy_model_to_eunit_dir(Filename, ModelDir) - end, - Filenames - ). - -%% @private -copy_model_to_eunit_dir(Filename, ModelDir) -> - %% We only want to copy erl files. If it's something else, - %% don't touch it. - case is_erl_file(Filename) of - true -> - Source = filename:join(ModelDir, Filename), - Destination = filename:join(".eunit", Filename), - file:copy(Source, Destination); - false -> - ok - end. - -%% @private -%% @doc simple helper to determine whether this file is an -%% erlang file -is_erl_file(Filename) -> - filename:extension(Filename) =:= ".erl". - -%% @private -%% @doc Modifications are only needed if both coverage is enabled -%% and the models are not in the src directory. -coverage_modifications_needed(RebarConf, BossDbOpts) -> - coverage_enabled(RebarConf) and model_dir_is_not_src(BossDbOpts). - -%% @private -%% @doc We only want to copy the models if they are stored outside of -%% the src directory. -model_dir_is_not_src(BossDbOpts) -> - ModelsDir = option(model_dir, BossDbOpts), - [FirstDir | _] = filename:split(ModelsDir), - FirstDir =/= "src". diff --git a/rebar.config b/rebar.config index 2cc12a53..889f842f 100644 --- a/rebar.config +++ b/rebar.config @@ -1,72 +1,63 @@ -%-*-Erlang-*- -% vim: ts=8 sw=8 ft=erlang +%%-*-Erlang-*- +%% vim: ts=8 sw=8 ft=erlang {erl_opts, [ - debug_info, - warn_unused_vars, - warn_unused_import, - warn_exported_vars, - {parse_transform, lager_transform}, - {parse_transform, cut}, - {parse_transform, do}, - {parse_transform, import_as} -]}. + debug_info, + tuple_calls, + warnings_as_errors, + warn_unused_vars, + warn_unused_import, + warn_exported_vars, + {parse_transform, cut}, + {parse_transform, do}, + {parse_transform, import_as} + ]}. {deps, [ - {lager, {git, "https://github.com/erlang-lager/lager.git", {tag, "3.6.7"}}}, - {erlando, {git, "https://github.com/ChicagoBoss/erlando.git", {ref, "680688f"}}}, - {aleppo, {git, "https://github.com/ErlyORM/aleppo.git", {tag, "v0.9.4"}}}, - {medici, {git, "https://github.com/ErlyORM/medici.git", {ref, "bb6167459d"}}}, - - % for Erlang 17 uncomment line below (using legacy branch of bson) - %{bson, {git, "https://github.com/comtihon/bson-erlang", {branch, "legacy"}}}, - %{mongodb, {git, "https://github.com/comtihon/mongodb-erlang", {tag, "v0.7.9"}}}, - - % riak_pb not compatible with Erlang 18, so commented for now - % uncomment line below if you need Riak support and have Erlang < 18 - %{riakc, {git, "https://github.com/ErlyORM/riak-erlang-client.git", {tag, "1.3.0-boss"}}}, + {erlando, {git, "https://github.com/burbas/erlando.git", {branch, "master"}}}, + {aleppo, {git, "https://github.com/burbas/aleppo.git", {branch, "master"}}}, + {dh_date, {git, "https://github.com/daleharvey/dh_date.git", {branch, "master"}}}, + {tiny_pq, {git, "https://github.com/ChicagoBoss/tiny_pq.git", {tag, "v0.9.0"}}}, + {poolboy, {git, "https://github.com/devinus/poolboy.git", {tag, "1.5.2"}}}, + {uuid, {git, "https://github.com/avtobiff/erlang-uuid.git", {branch, "master"}}}, + %% Database interfaces {ddb, {git, "https://github.com/ErlyORM/ddb.git", {tag, "v0.1.7"}}}, {epgsql, {git, "https://github.com/epgsql/epgsql.git", {tag, "4.2.0"}}}, {erlmc, {git, "https://github.com/ErlyORM/erlmc.git", {ref, "c5280da"}}}, - {mysql, {git, "https://github.com/ErlyORM/erlang-mysql-driver.git", {tag, "v0.0.4"}}}, - {poolboy, {git, "https://github.com/devinus/poolboy.git", {tag, "1.5.2"}}}, - {uuid, {git, "https://github.com/avtobiff/erlang-uuid.git", {tag, "v0.5.2"}}}, + {mysql, ".*", {git, "https://github.com/mysql-otp/mysql-otp", {tag, "1.8.0"}}}, {redo, {git, "https://github.com/heroku/redo.git", {ref, "cd75a11"}}}, - % boss_branch for ets_cache - {ets_cache, {git, "https://github.com/cuongth/ets_cache.git", {ref, "c7a17204cd"}}}, - {dh_date, {git, "https://github.com/daleharvey/dh_date.git", {ref, "23e5a61"}}}, - {tiny_pq, {git, "https://github.com/ChicagoBoss/tiny_pq.git", {tag, "v0.9.0"}}} - ]}. + {ets_cache, {git, "https://github.com/cuongth/ets_cache.git", {ref, "c7a17204cd"}}} + ]}. %% == Dialyzer == {dialyzer, [ - {warnings, [error_handling, race_conditions, unmatched_returns, underspecs]}, - {get_warnings, false}, - {plt_apps, top_level_deps}, % top_level_deps | all_deps - {plt_extra_apps, []}, - {plt_location, local}, % local | "/my/file/name" - {plt_prefix, "boss_db"}, - {base_plt_apps, [stdlib, kernel, erts]}, - {base_plt_location, global}, % global | "/my/file/name" - {base_plt_prefix, "rebar3"} -]}. + {warnings, [error_handling, unmatched_returns, underspecs]}, + {get_warnings, false}, + {plt_apps, top_level_deps}, % top_level_deps | all_deps + {plt_extra_apps, []}, + {plt_location, local}, % local | "/my/file/name" + {plt_prefix, "boss_db"}, + {base_plt_apps, [stdlib, kernel, erts]}, + {base_plt_location, global}, % global | "/my/file/name" + {base_plt_prefix, "rebar3"} + ]}. {cover_enabled, true}. {plugins, []}. {profiles, - [{test, [ - {deps, [ - {proper, {git, "https://github.com/manopapad/proper.git", {ref, "v1.3"}}}, - {boss_test, {git, "https://github.com/ChicagoBoss/boss_test.git", {tag, "0.0.1"}}} - ]} - ]}, - {boss_test, [ - {erl_opts, [{d, boss_test}]} - ]}, - {prod, [ - %{erl_opts, [warnings_as_errors]} - ]} -]}. + [{test, [ + {deps, [ + {proper, {git, "https://github.com/manopapad/proper.git", {ref, "v1.3"}}}, + {boss_test, {git, "https://github.com/ChicagoBoss/boss_test.git", {tag, "0.0.1"}}} + ]} + ]}, + {boss_test, [ + {erl_opts, [{d, boss_test}]} + ]}, + {prod, [ + {erl_opts, [warnings_as_errors]} + ]} + ]}. \ No newline at end of file diff --git a/rebar.config.script b/rebar.config.script deleted file mode 100644 index 003be308..00000000 --- a/rebar.config.script +++ /dev/null @@ -1,48 +0,0 @@ -case erlang:function_exported(rebar3, main, 1) of - true -> % rebar3 - CONFIG; - false -> % rebar 2.x or older - %% Rebuild deps, possibly including those that have been moved to - %% profiles - [{deps, [ - {lager, ".*", {git, "https://github.com/erlang-lager/lager.git", {tag, "3.6.7"}}}, - {erlando, ".*", {git, "https://github.com/ChicagoBoss/erlando.git", {tag, "680688f"}}}, - {aleppo, ".*", {git, "https://github.com/ErlyORM/aleppo.git", {tag, "v0.9.4"}}}, - {medici, ".*", {git, "https://github.com/ErlyORM/medici.git", {tag, "bb6167459d"}}}, - - % Different version of mongodb driver and bson breaking compilation - % on different Erlang version. So disabling them by default. Uncomment one - % of options below, if you need mongodb driver - % for Erlang = 17 - %{bson, ".*", {git, "https://github.com/comtihon/bson-erlang", {branch, "legacy"}}}, - %{mongodb, ".*", {git, "https://github.com/comtihon/mongodb-erlang", {tag, "v0.7.9"}}}, - % for Erlang >= 18 - %{mongodb, ".*", {git, "https://github.com/comtihon/mongodb-erlang", {tag, "v0.7.9"}}}, - - % riak_pb not compatible with Erlang 18, so commented for now - % uncomment line below if you need Riak support and have Erlang < 18 - %{riakc, ".*", {git, "https://github.com/ErlyORM/riak-erlang-client.git", {tag, "1.3.0-boss"}}}, - - {ddb, ".*", {git, "https://github.com/ErlyORM/ddb.git", {tag, "v0.1.7"}}}, - {epgsql, ".*", {git, "https://github.com/epgsql/epgsql.git", {tag, "4.2.0"}}}, - {erlmc, ".*", {git, "https://github.com/ErlyORM/erlmc.git", {tag, "c5280da"}}}, - {mysql, ".*", {git, "https://github.com/ErlyORM/erlang-mysql-driver.git", {tag, "v0.0.4"}}}, - {poolboy, ".*", {git, "https://github.com/devinus/poolboy.git", {tag, "1.5.2"}}}, - {uuid, ".*", {git, "https://github.com/avtobiff/erlang-uuid.git", {tag, "v0.5.2"}}}, - {redo, ".*", {git, "https://github.com/heroku/redo.git", {tag, "cd75a11"}}}, - % boss_branch for ets_cache - {ets_cache, ".*", {git, "https://github.com/cuongth/ets_cache.git", {tag, "c7a17204cd"}}}, - {proper, ".*", {git, "https://github.com/manopapad/proper.git", {tag, "v1.3"}}}, - {dh_date, ".*", {git, "https://github.com/daleharvey/dh_date.git", {tag, "23e5a61"}}}, - {tiny_pq, ".*", {git, "https://github.com/ChicagoBoss/tiny_pq.git", {tag, "v0.9.0"}}}, - {boss_test, ".*", {git, "https://github.com/ChicagoBoss/boss_test.git", {tag, "0.0.1"}}} - ]} - ,{plugins, [rebar_ct]} - ,{erl_opts, [ - debug_info, - {parse_transform, lager_transform}, - {parse_transform, cut}, - {parse_transform, do}, - {parse_transform, import_as} - ]} | [Config || {Key, _Value}=Config <- CONFIG, Key =/= deps andalso Key =/= plugins andalso Key =/= erl_opts]] -end. diff --git a/src/boss_compiler.erl b/src/boss_compiler.erl index 3d9b485d..02e7e30b 100644 --- a/src/boss_compiler.erl +++ b/src/boss_compiler.erl @@ -1,8 +1,6 @@ -module(boss_compiler). -export([compile/1, compile/2, parse/3]). --compile(export_all). - -ifdef(TEST). -compile(export_all). -endif. @@ -35,7 +33,7 @@ compile(File) -> compile(File, []). compile(File, Options) -> - _ = lager:notice("Compile file ~p with options ~p ", [File, Options]), + _ = logger:notice("Compile file ~p with options ~p ", [File, Options]), IncludeDirs = ["include"] ++ proplists:get_value(include_dirs, Options, []) ++ proplists:get_all_values(i, compiler_options(Options)), TokenTransform = proplists:get_value(token_transform, Options), diff --git a/src/boss_db.erl b/src/boss_db.erl index dd0526f3..10e822b7 100644 --- a/src/boss_db.erl +++ b/src/boss_db.erl @@ -1,7 +1,6 @@ %% @doc Chicago Boss database abstraction -module(boss_db). - -export([start/1, stop/0]). -export([ @@ -74,8 +73,14 @@ start(Options) -> AdapterName = proplists:get_value(adapter, Options, mock), Adapter = list_to_atom(lists:concat(["boss_db_adapter_", AdapterName])), - _ = lager:info("Start Database Adapter ~p options ~p", [Adapter, Options]), - Adapter:start(Options), + logger:info("Start Database Adapter ~p options ~p", [Adapter, Options]), + %% We need to check if there's an adapter + case erlang:function_exported(Adapter, start, 1) of + true -> + Adapter:start(Options); + _ -> + ok + end, lists:foldr(fun(ShardOptions, Acc) -> case proplists:get_value(db_shard_models, ShardOptions, []) of [] -> Acc; @@ -84,7 +89,12 @@ start(Options) -> undefined -> Adapter; ShortName -> list_to_atom(lists:concat(["boss_db_adapter_", ShortName])) end, - ShardAdapter:start(ShardOptions ++ Options), + case erlang:function_exported(ShardAdapter, start, 1) of + true -> + ShardAdapter:start(ShardOptions ++ Options); + _ -> + ok + end, Acc end end, [], proplists:get_value(shards, Options, [])), @@ -99,7 +109,10 @@ db_call(Msg) -> db_call(Msg, Timeout) when is_integer(Timeout), Timeout > 0 -> case erlang:get(boss_db_transaction_info) of undefined -> - boss_pool:call(?POOLNAME, Msg, ?DEFAULT_TIMEOUT); + Worker = poolboy:checkout(?POOLNAME, true, Timeout), + Reply = gen_server:call(Worker, Msg, Timeout), + poolboy:checkin(?POOLNAME, Worker), + Reply; State -> {reply, Reply, NewState} = boss_db_controller:handle_call(Msg, undefined, State), @@ -136,7 +149,7 @@ create_migration_table_if_needed() -> %% @doc Run database migration {Tag, Fun} in Direction migrate({Tag, Fun}, Direction) -> - _ = lager:info("Running migration: ~p ~p~n", [Tag, Direction]), + logger:info("Running migration: ~p ~p~n", [Tag, Direction]), Fun(Direction), db_call({migration_done, Tag, Direction}). diff --git a/src/boss_db_adapter.erl b/src/boss_db_adapter.erl index 7711b6ce..7173a067 100644 --- a/src/boss_db_adapter.erl +++ b/src/boss_db_adapter.erl @@ -2,7 +2,8 @@ %% TODO: exact types -callback start(_) -> ok. --callback stop() -> ok. +-optional_callbacks([start/1]). + -callback init(_) -> any(). -callback terminate(_) -> any(). -callback find(_, _) -> any(). @@ -12,3 +13,6 @@ -callback counter(_, _) -> any(). -callback incr(_, _, _) -> any(). -callback save_record(_, _) -> any(). + +-callback dump(_) -> any(). +-optional_callbacks([dump/1]). diff --git a/src/boss_db_controller.erl b/src/boss_db_controller.erl index 1f66a5ae..16bbc8b0 100644 --- a/src/boss_db_controller.erl +++ b/src/boss_db_controller.erl @@ -9,19 +9,21 @@ -define(MAXDELAY, 10000). -record(state, { - connection_state, - connection_delay, - connection_retry_timer, - options, - adapter, - read_connection, - write_connection, - shards = [], - model_dict = dict:new(), - cache_enable, - cache_ttl, - cache_prefix, - depth = 0}). + connection_state, + connection_delay, + connection_retry_timer, + options, + adapter, + read_connection, + write_connection, + shards = [], + model_dict = dict:new(), + cache_enable, + cache_ttl, + cache_prefix, + depth = 0, + supports_dump = false + }). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% start_link() -> @@ -85,14 +87,17 @@ init(Options) -> CacheTTL = proplists:get_value(cache_exp_time, Options, 60), CachePrefix = proplists:get_value(cache_prefix, Options, db), process_flag(trap_exit, true), + SupportsDump = erlang:function_exported(Adapter, dump, 1), try_connection(self(), Options), {ok, #state{connection_state = connecting, - connection_delay = 1, - options = Options, - adapter = Adapter, - cache_enable = CacheEnable, - cache_ttl = CacheTTL, - cache_prefix = CachePrefix }}. + connection_delay = 1, + options = Options, + adapter = Adapter, + cache_enable = CacheEnable, + cache_ttl = CacheTTL, + cache_prefix = CachePrefix, + supports_dump = SupportsDump + }}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -176,10 +181,12 @@ handle_call(pop, _From, State) -> handle_call(depth, _From, State) -> {reply, State#state.depth, State}; -handle_call(dump, _From, State) -> - Adapter = State#state.adapter, - Conn = State#state.read_connection, +handle_call(dump, _From, State = #state{adapter = Adapter, read_connection = Conn, + supports_dump = true}) -> {reply, Adapter:dump(Conn), State}; +handle_call(dump, _From, State = #state{adapter = Adapter, supports_dump = false}) -> + logger:error("Adapter ~s does not support the command 'dump'", [Adapter]), + {reply, {error, not_supported}, State}; handle_call({create_table, TableName, TableDefinition}, _From, State) -> Adapter = State#state.adapter, @@ -284,8 +291,8 @@ find_by_key(Key, From, Prefix, State, _CachedValue = undefined) -> {Prefix, Key}, State#state.cache_ttl); false -> - _ = lager:error("Find in Cache by key error ~p ~p ", [Key, Res]), - error + _ = logger:error("Find in Cache by key error ~p ~p ", [Key, Res]), + error end, {reply, Res, State}; find_by_key(Key, _From, _Prefix, State, CachedValue) -> diff --git a/src/boss_db_pt.erl b/src/boss_db_pt.erl index 597fd152..d7b92b8e 100644 --- a/src/boss_db_pt.erl +++ b/src/boss_db_pt.erl @@ -21,8 +21,6 @@ clause_body/1, clause_guard/1, match_expr/2, - function_clauses/1, - get_pos/1, add_ann/2, get_ann/1]). @@ -235,7 +233,7 @@ rpt_error(Reason, BeforeOrAfter, Fun, Info) -> fun({K,V}, Acc) -> [K, V | Acc] end, [], Info)], - lager:error(Fmt, Args). + logger:error(Fmt, Args). should_i_bind(Tree) -> erl_syntax_lib:fold( diff --git a/src/boss_db_sup.erl b/src/boss_db_sup.erl index 79efcc65..dc9e4dd4 100644 --- a/src/boss_db_sup.erl +++ b/src/boss_db_sup.erl @@ -16,6 +16,6 @@ start_link(StartArgs) -> init(StartArgs) -> Args = StartArgs ++ [{name, {local, boss_db_pool}}, {worker_module, boss_db_controller}, - {size, 5}, {max_overflow, 10}], + {size, 3}, {max_overflow, 0}], PoolSpec = {db_controller, {poolboy, start_link, [Args]}, permanent, 2000, worker, [poolboy]}, {ok, {{one_for_one, 10, 10}, [PoolSpec]}}. diff --git a/src/boss_db_test_app.erl b/src/boss_db_test_app.erl index c6b5c018..3ca8280f 100644 --- a/src/boss_db_test_app.erl +++ b/src/boss_db_test_app.erl @@ -48,7 +48,7 @@ run_setup() -> lists:map(fun("") -> ok; (Cmd) -> RetVal = boss_db:execute(Cmd), - lager:info("Returned: ~p~n", [RetVal]) + logger:info("Returned: ~p~n", [RetVal]) end, re:split(FileContents, ";")); {error, _Reason} -> ok @@ -59,7 +59,7 @@ run_tests() -> boss_db:mock_transaction(fun run_tests_inner/0). run_tests_inner() -> - _ = lager:info("~-60s", ["Root test"]), + logger:info("~-60s", ["Root test"]), ModelText = <<"Economists do it with models">>, do( fun() -> diff --git a/src/boss_news.erl b/src/boss_news.erl index 85193cd2..2d78dbf3 100644 --- a/src/boss_news.erl +++ b/src/boss_news.erl @@ -1,18 +1,23 @@ -module(boss_news). --export([start/0, start/1]). - --export([stop/0]). - --export([watch/2, watch/3, watch/4]). - --export([set_watch/3, set_watch/4, set_watch/5]). - --export([cancel_watch/1, extend_watch/1]). - --export([deleted/2, updated/3, created/2]). - --export([reset/0, dump/0]). +-export([ + start/0, + start/1, + stop/0, + watch/2, + watch/3, + watch/4, + set_watch/3, + set_watch/4, + set_watch/5, + cancel_watch/1, + extend_watch/1, + deleted/2, + updated/3, + created/2, + reset/0, + dump/0 + ]). -define(TRILLION, 1000 * 1000 * 1000 * 1000). @@ -42,7 +47,7 @@ watch(TopicString, CallBack, UserInfo) -> %% @doc Same as `watch/3', except that the watch expires after `TTL' seconds. %% @spec watch( TopicString :: string(), CallBack, UserInfo, TTL) -> {ok, WatchId} | {error, Reason} watch(TopicString, CallBack, UserInfo, TTL) -> - gen_server:call({global, ?MODULE}, {watch, TopicString, CallBack, UserInfo, TTL}). + call_server({watch, TopicString, CallBack, UserInfo, TTL}). %% @doc Create or replace a watch with `WatchId'. %% @spec set_watch( WatchId, TopicString::string(), CallBack ) -> ok | {error, Reason} @@ -57,29 +62,41 @@ set_watch(WatchId, TopicString, CallBack, UserInfo) -> %% @doc Same as `set_watch/4', except that the watch expires after `TTL' seconds. %% @spec set_watch( WatchId, TopicString::string(), CallBack, UserInfo, TTL ) -> ok | {error, Reason} set_watch(WatchId, TopicString, CallBack, UserInfo, TTL) -> - gen_server:call({global, ?MODULE}, {set_watch, WatchId, TopicString, CallBack, UserInfo, TTL}). + call_server({set_watch, WatchId, TopicString, CallBack, UserInfo, TTL}). %% @doc Cancel an existing watch identified by `WatchId'. %% @spec cancel_watch( WatchId ) -> ok | {error, Reason} cancel_watch(WatchId) -> - gen_server:call({global, ?MODULE}, {cancel_watch, WatchId}). + call_server({cancel_watch, WatchId}). %% @doc Extend an existing watch by the time-to-live specified at creation time. %% @spec extend_watch( WatchId ) -> ok | {error, Reason} extend_watch(WatchId) -> - gen_server:call({global, ?MODULE}, {extend_watch, WatchId}). + call_server({extend_watch, WatchId}). deleted(Id, Attrs) -> - gen_server:call({global, ?MODULE}, {deleted, Id, Attrs}). + call_server({deleted, Id, Attrs}). updated(Id, OldAttrs, NewAttrs) -> - gen_server:call({global, ?MODULE}, {updated, Id, OldAttrs, NewAttrs}). + call_server({updated, Id, OldAttrs, NewAttrs}). created(Id, NewAttrs) -> - gen_server:call({global, ?MODULE}, {created, Id, NewAttrs}). + call_server({created, Id, NewAttrs}). reset() -> - gen_server:call({global, ?MODULE}, reset). + call_server(reset). dump() -> - gen_server:call({global, ?MODULE}, dump). + call_server(dump). + + +%%%%%%%%%%%%%%%%%%%%%% +%% Private functions +%%%%%%%%%%%%%%%%%%%%%% +call_server(Msg) -> + case boss_news_sup:is_started() of + true -> + gen_server:call({global, ?MODULE}, Msg); + _ -> + {error, not_started} + end. diff --git a/src/boss_news_controller.erl b/src/boss_news_controller.erl index ad49a160..c8721b04 100644 --- a/src/boss_news_controller.erl +++ b/src/boss_news_controller.erl @@ -1,7 +1,6 @@ -module(boss_news_controller). -behaviour(gen_server). - -ifdef(TEST). -compile(export_all). -endif. diff --git a/src/boss_news_sup.erl b/src/boss_news_sup.erl index e7c2411f..a7061c0b 100644 --- a/src/boss_news_sup.erl +++ b/src/boss_news_sup.erl @@ -1,10 +1,12 @@ -module(boss_news_sup). - -behaviour(supervisor). --export([start_link/0, start_link/1]). - --export([init/1]). +-export([ + start_link/0, + start_link/1, + init/1, + is_started/0 + ]). start_link() -> start_link([]). @@ -12,6 +14,9 @@ start_link() -> start_link(StartArgs) -> supervisor:start_link({global, ?MODULE}, ?MODULE, StartArgs). +is_started() -> + global:whereis_name({global, ?MODULE}) /= {error, no_proc}. + init(StartArgs) -> {ok, {{one_for_one, 10, 10}, [ {news_controller, {boss_news_controller, start_link, [StartArgs]}, diff --git a/src/boss_record_compiler.erl b/src/boss_record_compiler.erl index 982a5ec6..cb507893 100644 --- a/src/boss_record_compiler.erl +++ b/src/boss_record_compiler.erl @@ -3,10 +3,11 @@ -author('zkessin@gmail.com'). -define(DATABASE_MODULE, boss_db). -define(PREFIX, "BOSSRECORDINTERNAL"). + -ifdef(TEST). -compile(export_all). -endif. --compile(export_all). + -type limit() :: pos_integer() | 'all' | 'many'. -type error(T) :: {ok, T} | {error, string()}. -type syntaxTree() :: erl_syntax:syntaxTree() | any(). @@ -117,7 +118,7 @@ edoc_module(File, Options) -> Options). process_tokens(Tokens) -> - _ = lager:info("Tokens ~p",[Tokens]), + logger:info("Tokens ~p",[Tokens]), process_tokens(Tokens, [], []). process_tokens([{']',_}, @@ -133,7 +134,6 @@ process_tokens([{'-',N } = T1, {'[',_ } = T6, {var,_,'Id' } = T7| Rest] = _T, TokenAcc, []) -> - % lager:notice("Tokens ~p", [_T]), process_tokens(Rest, lists:reverse([T1, T2, T3, T4, T5, T6, T7], TokenAcc), []); %-module(Foo, [...]) with type specs process_tokens([{'-',_N } = T1, @@ -148,8 +148,7 @@ process_tokens([{'-',_N } = T1, {'(',_}, {')',_}|Rest] = _T, TokenAcc, []) -> - % lager:notice("Tokens ~p", [_T]) , - _ = lager:info("Var Type ~p",[VarType]), + logger:info("Var Type ~p",[VarType]), process_tokens(Rest, lists:reverse([T1, T2, T3, T4, T5, T6, T7], TokenAcc), [{'Id', VarType}]); process_tokens([{',',_} = T1, @@ -159,8 +158,7 @@ process_tokens([{',',_} = T1, {'(',_}, {')',_} |Rest] = _T, TokenAcc, Acc) -> -% lager:notice("Tokens ~p", [_T]), - _ = lager:info("Var Type ~p",[VarType]), + logger:info("Var Type ~p",[VarType]), process_tokens(Rest, lists:reverse([T1, T2], TokenAcc), [{VarName, VarType}|Acc]); process_tokens([H|T], TokenAcc, Acc) -> @@ -216,13 +214,13 @@ make_generated_forms(ModuleName, Parameters, _TokenInfo, _Attributes, make_generated_forms(ModuleName, Parameters, _TokenInfo, _Attributes, _Counters, _Dup = true) -> DupFields = Parameters -- sets:to_list(sets:from_list(Parameters)), - _ = lager:error("Unable to compile module ~p due to duplicate field(s) ~p", + logger:error("Unable to compile module ~p due to duplicate field(s) ~p", [ModuleName, DupFields]), {error, "Duplicate Fields"}; make_generated_forms(ModuleName, Parameters, TokenInfo, Attributes, Counters, _Dup = false) -> - _ = lager:notice("Module \"~p\" Parameters ~p Attributes~p", [ModuleName,Parameters, Attributes]), + logger:debug("Module \"~p\" Parameters ~p Attributes~p", [ModuleName,Parameters, Attributes]), GF = attribute_names_forms(ModuleName, Parameters) ++ attribute_types_forms(ModuleName, TokenInfo) ++ database_columns_forms(ModuleName, Parameters, Attributes) ++ diff --git a/src/boss_record_lib.erl b/src/boss_record_lib.erl index 117e42ba..3604f27a 100644 --- a/src/boss_record_lib.erl +++ b/src/boss_record_lib.erl @@ -126,6 +126,8 @@ ensure_loaded(Module) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% convert_value_to_type(Val, undefined) -> Val; +convert_value_to_type(null, _) -> + null; convert_value_to_type(Val, integer) when is_integer(Val) -> Val; convert_value_to_type(Val, integer) when is_list(Val) -> diff --git a/src/boss_sql_lib.erl b/src/boss_sql_lib.erl index 57bc6fd6..96101971 100644 --- a/src/boss_sql_lib.erl +++ b/src/boss_sql_lib.erl @@ -36,7 +36,7 @@ convert_id_condition_to_use_table_ids({Key, Op, Value}) when Op =:= 'equals'; Op {Key, Op, TableId}; convert_id_condition_to_use_table_ids({Key, Op, {Min, Max}}) when Op =:= 'in'; Op =:= 'not_in' -> {_Type, _TableName, _IdColumn, TableId1} = infer_type_from_id(Min), - {_Type, _TableName, _IdColumn, TableId2} = infer_type_from_id(Max), + {_Type0, _TableName0, _IdColumn0, TableId2} = infer_type_from_id(Max), {Key, Op, {TableId1, TableId2}}; convert_id_condition_to_use_table_ids({Key, Op, ValueList}) when is_list(ValueList) andalso (Op =:= 'in' orelse Op =:= 'not_in') -> Value2 = lists:map(fun(V) -> diff --git a/src/cache_adapters/boss_cache_adapter_ets.erl b/src/cache_adapters/boss_cache_adapter_ets.erl index 70ff628d..210a714d 100644 --- a/src/cache_adapters/boss_cache_adapter_ets.erl +++ b/src/cache_adapters/boss_cache_adapter_ets.erl @@ -22,12 +22,12 @@ start() -> start(Options) -> case check_server:start_link(Options) of - {ok, CheckPid} -> lager:info("ETS check server started"); - {error, {already_started, CheckPid}} -> lager:warning("ETS check server already started") + {ok, CheckPid} -> logger:info("ETS check server started"); + {error, {already_started, CheckPid}} -> logger:warning("ETS check server already started") end, case cache_server:start_link([{checkpid, CheckPid}|Options]) of - {ok, Conn} -> lager:info("ETS cache server started"); - {error, {already_started, Conn}} -> lager:warning("ETS cache server already started") + {ok, Conn} -> logger:info("ETS cache server started"); + {error, {already_started, Conn}} -> logger:warning("ETS cache server already started") end, Conn. diff --git a/src/db_adapters/boss_db_adapter_dynamodb.erl b/src/db_adapters/boss_db_adapter_dynamodb.erl index ed1f65ad..1d14b8d2 100644 --- a/src/db_adapters/boss_db_adapter_dynamodb.erl +++ b/src/db_adapters/boss_db_adapter_dynamodb.erl @@ -1,24 +1,31 @@ -module(boss_db_adapter_dynamodb). -behaviour(boss_db_adapter). --export([start/1, stop/0, init/1, terminate/1, find/2, find/7]). --export([count/3, counter/2, incr/2, incr/3, delete/2, save_record/2]). --export([push/2, pop/2]). +-export([ + start/1, + init/1, + terminate/1, + find/2, + find/7, + count/3, + counter/2, + incr/2, + incr/3, + delete/2, + save_record/2, + push/2, + pop/2 + ]). -ifdef(TEST). +%% Used for exposing all functions in testing -compile(export_all). -endif. --define(LOG(Name, Value), lager:debug("DEBUG: ~s: ~p~n", [Name, Value])). - -% Number of seconds between beginning of gregorian calendar and 1970 --define(GREGORIAN_SECONDS_1970, 62167219200). +-include("../../include/boss_db.hrl"). start(_Options) -> application:start(ddb). -stop() -> - ok. - init(Options) -> AccessKey = proplists:get_value(db_username, Options, os:getenv("AWS_ACCESS_KEY_ID")), SecretKey = proplists:get_value(db_password, Options, os:getenv("AWS_SECRET_ACCESS_KEY")), @@ -27,7 +34,6 @@ init(Options) -> %% startup dependencies. some of these may have already been started, but that's ok. inets:start(), ssl:start(), - %%lager:start(), application:start(ibrowse), @@ -395,7 +401,7 @@ ddb_type_of(Value) when is_number(Value) -> ddb_type_of(_Value) -> <<"S">>. --spec(operator_to_ddb(boss_db_adapter_types:model_operator()) -> +-spec(operator_to_ddb(model_operator()) -> binary()). operator_to_ddb('equals') -> <<"EQ">>; diff --git a/src/db_adapters/boss_db_adapter_mnesia.erl b/src/db_adapters/boss_db_adapter_mnesia.erl index 1fc69919..55fe5883 100644 --- a/src/db_adapters/boss_db_adapter_mnesia.erl +++ b/src/db_adapters/boss_db_adapter_mnesia.erl @@ -1,6 +1,6 @@ -module(boss_db_adapter_mnesia). -behaviour(boss_db_adapter). --export([init/1, terminate/1, start/1, stop/0, find/2, find/7]). +-export([init/1, terminate/1, start/1, find/2, find/7]). -export([count/3, counter/2, incr/3, delete/2, save_record/2]). -export([transaction/2]). -export([table_exists/2, get_migrations_table/1, migration_done/3]). @@ -10,9 +10,6 @@ start(_) -> application:start(mnesia). -stop() -> - application:stop(mnesia). - % ----- init(_Options) -> @@ -20,7 +17,7 @@ init(_Options) -> % ----- terminate(_) -> - ok. + application:stop(mnesia). % ----- find(_, Id) when is_list(Id) -> @@ -271,5 +268,3 @@ build_conditions1([{Key, 'eq', Value}|Rest], Pattern, Filter) -> build_conditions1(Rest, lists:keystore(Key, 1, Pattern, {Key, Value}), Filter); build_conditions1([First|Rest], Pattern, Filter) -> build_conditions1(Rest, Pattern, [First|Filter]). - - diff --git a/src/db_adapters/boss_db_adapter_mock.erl b/src/db_adapters/boss_db_adapter_mock.erl index a80e2fb8..08e79c27 100644 --- a/src/db_adapters/boss_db_adapter_mock.erl +++ b/src/db_adapters/boss_db_adapter_mock.erl @@ -1,7 +1,7 @@ % In-memory database for fast tests and easy setup -module(boss_db_adapter_mock). -behaviour(boss_db_adapter). --export([init/1, terminate/1, start/1, stop/0]). +-export([init/1, terminate/1, start/1]). -export([find/2, find/7, count/3, counter/2, incr/3, delete/2, save_record/2]). -export([push/2, pop/2, dump/1, transaction/2]). @@ -13,9 +13,6 @@ start(Options) -> ok end. -stop() -> - ok. - init(_Options) -> {ok, undefined}. diff --git a/src/db_adapters/boss_db_adapter_mongodb.erl b/src/db_adapters/boss_db_adapter_mongodb.erl deleted file mode 100644 index c76925b1..00000000 --- a/src/db_adapters/boss_db_adapter_mongodb.erl +++ /dev/null @@ -1,657 +0,0 @@ --module(boss_db_adapter_mongodb). --behaviour(boss_db_adapter). -%-include_lib("mongodb/include/mongo_protocol.hrl"). --export([start/1, stop/0, init/1, terminate/1, find/2, find/7]). --export([count/3, counter/2, incr/2, incr/3, delete/2, save_record/2]). --export([execute/2, transaction/2]). --export([push/2, pop/2, dump/1]). --export([table_exists/2, get_migrations_table/1, migration_done/3]). - --define(LOG(Name, Value), lager:debug("DEBUG: ~s: ~p~n", [Name, Value])). --type maybe(X) :: X|undefined. --type error_m(X) :: X|{error, any()}. - -% Number of seconds between beginning of gregorian calendar and 1970 --define(GREGORIAN_SECONDS_1970, 62167219200). --compile(export_all). --ifdef(TEST). --compile(export_all). --endif. -% JavaScript expression formats to query MongoDB --define(CONTAINS_FORMAT, "this.~s.indexOf('~s') != -1"). --define(NOT_CONTAINS_FORMAT, "this.~s.indexOf('~s') == -1"). --type db_op() :: 'not_equals'|'gt'|'ge'|'lt'|'le'|'in'|'not_in'. --type mongo_op() :: '$ne'|'$gt'|'$gte'|'$lt'|'$lte'|'$in'|'$nin'. --type read_mode() :: 'master'|'slave_ok'. --type proplist(Key,Value) :: [{Key, Value}]. --type proplist() :: proplist(any(), any()). - --spec boss_to_mongo_op(db_op()) -> mongo_op(). --spec pack_sort_order('ascending' | 'descending') -> -1 | 1. --spec tuple_to_proplist(tuple()) -> [{_,_}].% Tuple size must be even --spec proplist_to_tuple([{any(),any()}]) -> tuple(). --spec dec2hex(binary()) -> bitstring(). --spec dec2hex(bitstring(),binary()) -> bitstring(). --spec hex2dec(binary() | [byte(),...]) -> bitstring(). --spec hex2dec(bitstring(),binary()) -> bitstring(). --spec dec0(byte()) -> integer(). --spec hex0(byte()) -> 1..1114111. - - -start(_Options) -> - application:start(mongodb). - -stop() -> - ok. - -init(Options) -> - Database = proplists:get_value(db_database, Options, "test"), - WriteMode = proplists:get_value(db_write_mode, Options, safe), - ReadMode = proplists:get_value(db_read_mode, Options, master), - - ReadConnection = make_read_connection(Options, ReadMode), - WriteConnection = make_write_connection(Options, ReadConnection), - % We pass around arguments required by mongo:do/5 - case {proplists:get_value(db_username, Options),proplists:get_value(db_password, Options)} of - {undefined,undefined} -> - {ok, {readwrite, - {WriteMode, ReadMode, ReadConnection, list_to_atom(Database)}, - {WriteMode, ReadMode, WriteConnection, list_to_atom(Database)}} - }; - {User, Pass} -> - {ok, {readwrite, - {WriteMode, ReadMode, ReadConnection, list_to_atom(Database),list_to_binary(User),list_to_binary(Pass)}, - {WriteMode, ReadMode, WriteConnection, list_to_atom(Database),list_to_binary(User),list_to_binary(Pass)}} - } - end. - -make_write_connection(Options, ReadConnection) -> - case proplists:get_value(db_write_host, Options) of - undefined -> - ReadConnection; - WHost -> - WPort = proplists:get_value(db_write_host_port, Options, 27017), - {ok, WConn} = mongo:connect({WHost, WPort}), - WConn - end. - --spec(make_write_connection(proplist(),read_mode()) -> error_m(mongo:connection())). -make_read_connection(Options, ReadMode) -> - case proplists:get_value(db_replication_set, Options) of - undefined -> - Host = proplists:get_value(db_host, Options, "localhost"), - Port = proplists:get_value(db_port, Options, 27017), - {ok, Conn} = mongo:connect({Host, Port}), - Conn; - ReplSet -> - RSConn = mongo:rs_connect(ReplSet), - case read_connect1(ReadMode, RSConn) of - {ok, RSConn1} -> - RSConn1; - Error = {error,_} -> - Error - end - end. --spec(read_connect1(read_mode(), mongo:rs_connection()) -> - error_m( mongo:connection())). - -read_connect1(master, RSConn) -> - mongo_replset:primary(RSConn); -read_connect1(slave_ok, RSConn) -> - mongo_replset:secondary_ok(RSConn). - -terminate({_, _, Connection, _}) -> - case element(1, Connection) of - connection -> mongo:disconnect(Connection); - rs_connection -> mongo:rs_disconnect(Connection) - end; - -terminate({_, _, Connection, _,_,_}) -> - case element(1, Connection) of - connection -> mongo:disconnect(Connection); - rs_connection -> mongo:rs_disconnect(Connection) - end. - - -execute({WriteMode, ReadMode, Connection, Database}, Fun) -> - mongo:do(WriteMode, ReadMode, Connection, Database, Fun) ; -execute({WriteMode, ReadMode, Connection, Database, User, Password}, Fun) -> - mongo:do(WriteMode, ReadMode, Connection, Database, - fun() -> - case mongo:auth(User,Password) of - true -> - Fun(); - _ -> - _ = lager:error("Mongo DB Login Error check username and password ~p:~p", [User,Password]), - {error,bad_login} - end - end). - -% Transactions are not currently supported, but we'll treat them as if they are. -% Use at your own risk! -transaction(_Conn, TransactionFun) -> - {atomic, TransactionFun()}. - -find(Conn, Id) when is_list(Id) -> - {Type, Collection, MongoId} = infer_type_from_id(Id), - - Res = execute(Conn, fun() -> - mongo:find_one(Collection, {'_id', MongoId}) - end), - case Res of - {ok, {}} -> undefined; - {ok, {Doc}} -> mongo_tuple_to_record(Type, Doc); - {failure, Reason} -> {error, Reason} - - end. - -find(Conn, Type, Conditions, Max, Skip, Sort, SortOrder) when is_atom(Type), - is_list(Conditions), - is_integer(Max) orelse Max =:= all, - is_integer(Skip), - is_atom(Sort), - is_atom(SortOrder) -> - case boss_record_lib:ensure_loaded(Type) of - true -> - Collection = type_to_collection(Type), - Res = execute_find(Conn, Conditions, Max, Skip, Sort, SortOrder, - Collection), - case Res of - {ok, Curs} -> - lists:map(fun(Row) -> - mongo_tuple_to_record(Type, Row) - end, mongo:rest(Curs)); - {failure, Reason} -> {error, Reason} - - end; - false -> {error, {module_not_loaded, Type}} - end. - -execute_find(Conn, Conditions, Max, Skip, Sort, SortOrder, - Collection) -> - execute(Conn, fun() -> - Selector = build_conditions(Conditions, {Sort, pack_sort_order(SortOrder)}), - case Max of - all -> mongo:find(Collection, Selector, [], Skip); - _ -> mongo:find(Collection, Selector, [], Skip, Max) - end - end). - - -count(Conn, Type, Conditions) -> - Collection = type_to_collection(Type), - {ok, Count} = execute(Conn, fun() -> - C = build_conditions(Conditions), - % ?LOG("Conditions", C), - mongo:count(Collection, C) - end), - Count. - -counter(Conn, Id) when is_list(Id) -> - Res = execute(Conn, fun() -> - mongo:find_one(boss_counters, {'name', list_to_binary(Id)}) - end), - case Res of - {ok, {Doc}} -> - PropList = tuple_to_proplist(Doc), - proplists:get_value(value, PropList); - {failure, Reason} -> {error, Reason} - - end. - -incr(Conn, Id) -> - incr(Conn, Id, 1). - -incr(Conn, Id, Count) -> - Res = execute(Conn, fun() -> - mongo:repsert(boss_counters, - {'name', list_to_binary(Id)}, - {'$inc', {value, Count}} - ) - end), - case Res of - {ok, ok} -> counter(Conn, Id); - {failure, Reason} -> {error, Reason} - - end. - -delete(Conn, Id) when is_list(Id) -> - {_Type, Collection, MongoId} = infer_type_from_id(Id), - - Res = execute(Conn, fun() -> - mongo:delete(Collection, {'_id', MongoId}) - end), - resolve(Res). - - -save_record(Conn, Record) when is_tuple(Record) -> - Type = element(1, Record), - Collection = type_to_collection(Type), - Res = case Record:id() of - id -> - execute_save_record(Conn, Record, Collection); - DefinedId when is_list(DefinedId) -> - execute_save_record(Conn, Record, Collection, DefinedId) - end, - case Res of - {ok, ok} -> {ok, Record}; - {ok, Id} -> {ok, Record:set(id, unpack_id(Type, Id))}; - {failure, Reason} -> {error, Reason} - - end. - -execute_save_record(Conn, Record, Collection, DefinedId) -> - PackedId = pack_id(DefinedId), - PropList = lists:map(fun - ({id,_}) -> {'_id', PackedId}; - ({K,V}) -> - PackedVal = pack_value(K, V), - {K, PackedVal} - end, Record:attributes()), - Doc = proplist_to_tuple(PropList), - execute(Conn, fun() -> - mongo:repsert(Collection, {'_id', PackedId}, Doc) - end). - -pack_value(K, V) -> - case is_id_attr(K) of - true -> pack_id(V); - false -> pack_value(V) - end. - -execute_save_record(Conn, Record, Collection) -> - PropList = lists:foldr(fun - ({id,_}, Acc) -> Acc; - ({K,V}, Acc) -> - PackedVal = pack_value(K, V), - [{K, PackedVal}|Acc] - end, [], Record:attributes()), - Doc = proplist_to_tuple(PropList), - execute(Conn, fun() -> - mongo:insert(Collection, Doc) - end). - -% These 3 functions are not part of the behaviour but are required for -% tests to pass -push(_Conn, _Depth) -> ok. -pop(_Conn, _Depth) -> ok. -dump(_Conn) -> ok. - -% This is needed to support boss_db:migrate -table_exists(_Conn, _TableName) -> ok. - -resolve(_Res ={ok, _}) -> - ok; -resolve(_Res = {failure, Reason}) -> - {error, Reason}; -resolve(_Res = {connection_failure, Reason}) -> - _ = lager:error("connection failure ~p", [Reason]), - {error, Reason}. - - - -get_migrations_table(Conn) -> - Res = execute(Conn, fun() -> - mongo:find(schema_migrations, {}) - end), - resolve(Res). - -make_curser(Curs) -> - lists:map(fun(Row) -> - MongoDoc = tuple_to_proplist(Row), - {attr_value('id', MongoDoc), - attr_value(version, MongoDoc), - attr_value(migrated_at, MongoDoc)} - end, mongo:rest(Curs)). - -migration_done(Conn, Tag, up) -> - Res = execute(Conn, fun() -> - Doc = {version, pack_value(atom_to_list(Tag)), migrated_at, pack_value(os:timestamp())}, - mongo:insert(schema_migrations, Doc) - end), - resolve(Res); - -migration_done(Conn, Tag, down) -> - Res = execute(Conn, fun() -> - mongo:delete(schema_migrations, {version, pack_value(atom_to_list(Tag))}) - end), - resolve(Res). - - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% Internal functions -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -%% -%% Query generation -%% - -build_conditions(Conditions) -> - proplist_to_tuple(build_conditions1(Conditions, [])). - -build_conditions(Conditions, OrderBy) -> - {'query', build_conditions(Conditions), orderby, OrderBy}. - -build_conditions1([], Acc) -> - Acc; - -build_conditions1([{Key, 'matches', Value, Options}|Rest], Acc) -> - MongoOptions = mongo_regex_options_for_re_module_options(Options), - build_conditions1(Rest, [{Key, {regex, list_to_binary(Value), list_to_binary(MongoOptions)}}|Acc]); - -build_conditions1([{Key, 'not_matches', Value, Options}|Rest], Acc) -> - MongoOptions = mongo_regex_options_for_re_module_options(Options), - build_conditions1(Rest, [{Key, {'$not', {regex, list_to_binary(Value), list_to_binary(MongoOptions)}}}|Acc]); - -build_conditions1([{id, Operator, Value}|Rest], Acc) -> - build_conditions1([{'_id', Operator, Value}|Rest], Acc); - -build_conditions1([{Key, Operator, Value}|Rest], Acc) -> - Condition = build_conditions2(Key, Operator, Value), - % ?LOG("Condition", Condition), - build_conditions1(Rest, lists:append(Condition, Acc)). - - --spec(build_conditions2(string(),boss_db_adapter_types:model_operator(),string()) -> - [ - {atom(), binary()} | - {string(), {regex, binary(),binary()}} | - {string(), {'$not', {regex, binary(), binary()}}} - ]). -build_conditions2(Key, Operator, Value) -> - case {Operator, Value} of - {'not_matches', "*" ++ Value1} -> - [{Key, {'$not', {regex, list_to_binary(Value1), <<"i">>}}}]; - {'not_matches', Value} -> - [{Key, {'$not', {regex, list_to_binary(Value), <<"">>}}}]; - {'matches', "*" ++ Value1} -> - [{Key, {regex, list_to_binary(Value1), <<"i">>}}]; - {'matches', Value} -> - [{Key, {regex, list_to_binary(Value), <<"">>}}]; - {'contains', Value} -> - WhereClause = where_clause( - ?CONTAINS_FORMAT, [Key, Value]), - [{'$where', WhereClause}]; - {'not_contains', Value} -> - WhereClause = where_clause( - ?NOT_CONTAINS_FORMAT, [Key, Value]), - [{'$where', WhereClause}]; - {'contains_all', ValueList} -> - WhereClause = multiple_where_clauses( - ?CONTAINS_FORMAT, Key, ValueList, "&&"), - [{'$where', WhereClause}]; - {'not_contains_all', ValueList} -> - WhereClause = "!(" ++ multiple_where_clauses_string( - ?CONTAINS_FORMAT, Key, ValueList, "&&") ++ ")", - [{'$where', erlang:iolist_to_binary(WhereClause)}]; - {'contains_any', ValueList} -> - WhereClause = multiple_where_clauses( - ?CONTAINS_FORMAT, Key, ValueList, "||"), - [{'$where', WhereClause}]; - {'contains_none', ValueList} -> - WhereClause = multiple_where_clauses( - ?NOT_CONTAINS_FORMAT, Key, ValueList, "&&"), - [{'$where', WhereClause}]; - {'equals', Value} when is_list(Value) -> - case is_id_attr(Key) of - true -> - [{Key, pack_id(Value)}]; - false -> - [{Key, list_to_binary(Value)}] - end; - {'not_equals', Value} when is_list(Value) -> - [{Key, {'$ne', list_to_binary(Value)}}]; - {'equals', {{_,_,_},{_,_,_}} = Value} -> - [{Key, datetime_to_now(Value)}]; - {'equals', Value} -> - [{Key, Value}]; - {Operator, {{_,_,_},{_,_,_}} = Value} -> - [{Key, {boss_to_mongo_op(Operator), datetime_to_now(Value)}}]; - {'in', [H|T]} -> - [{Key, {'$in', lists:map(list_pack_function(Key), [H|T])}}]; - {'in', {Min, Max}} -> - [{Key, {'$gte', Min}}, {Key, {'$lte', Max}}]; - {'not_in', [H|T]} -> - [{Key, {'$nin', lists:map(list_pack_function(Key), [H|T])}}]; - {'not_in', {Min, Max}} -> - [{'$or', [{Key, {'$lt', Min}}, {Key, {'$gt', Max}}]}]; - {Operator, Value} -> - [{Key, {boss_to_mongo_op(Operator), Value}}] - end. - -list_pack_function(Key) -> - case is_id_attr(Key) of - true -> - fun(Id) -> pack_id(Id) end; - false -> - fun(Value) when is_list(Value) -> - list_to_binary(Value); - (Value) -> - Value - end - end. - - -where_clause(Format, Params) -> - erlang:iolist_to_binary( - io_lib:format(Format, Params)). - -multiple_where_clauses_string(Format, Key, ValueList, Operator) -> - ClauseList = lists:map(fun(Value) -> - lists:flatten(io_lib:format(Format, [Key, Value])) - end, ValueList), - string:join(ClauseList, " " ++ Operator ++ " "). - -multiple_where_clauses(Format, Key, ValueList, Operator) -> - erlang:iolist_to_binary(multiple_where_clauses_string(Format, Key, - ValueList, Operator)). - - -%% -%% Boss models introspection -%% - -infer_type_from_id(Id) when is_list(Id) -> - [Type, _BossId] = string:tokens(Id, "-"), - {list_to_atom(Type), type_to_collection(Type), pack_id(Id)}. - -is_id_attr(AttrName) -> - lists:suffix("_id", atom_to_list(AttrName)). - - -%% -%% Conversion between Chicago Boss en MongoDB -%% - -% Find MongoDB collection from Boss type -type_to_collection(Type) -> - list_to_atom(type_to_collection_name(Type)). - -type_to_collection_name(Type) when is_atom(Type) -> - type_to_collection_name(atom_to_list(Type)); -type_to_collection_name(Type) when is_list(Type) -> - inflector:pluralize(Type). - -% Convert a tuple return by the MongoDB driver to a Boss record -mongo_tuple_to_record(Type, Row) -> - MongoDoc = tuple_to_proplist(Row), - AttributeTypes = boss_record_lib:attribute_types(Type), - AttributeNames = boss_record_lib:attribute_names(Type), - Args = mongo_make_args(Type, MongoDoc, AttributeTypes, - AttributeNames), - apply(Type, new, Args). - - -mongo_make_args(Type, MongoDoc, AttributeTypes, AttributeNames) -> - lists:map(fun - (id) -> - MongoValue = attr_value(id, MongoDoc), - unpack_id(Type, MongoValue); - (AttrName) -> - MongoValue = attr_value(AttrName, MongoDoc), - ValueType = proplists:get_value(AttrName, AttributeTypes), - unpack_value(AttrName, MongoValue, ValueType) - end, - AttributeNames). - -mongo_regex_options_for_re_module_options(Options) -> - mongo_regex_options_for_re_module_options(Options, []). - -mongo_regex_options_for_re_module_options([], Acc) -> - lists:reverse(Acc); -mongo_regex_options_for_re_module_options([caseless|Rest], Acc) -> - mongo_regex_options_for_re_module_options(Rest, [$i|Acc]); -mongo_regex_options_for_re_module_options([dotall|Rest], Acc) -> - mongo_regex_options_for_re_module_options(Rest, [$s|Acc]); -mongo_regex_options_for_re_module_options([extended|Rest], Acc) -> - mongo_regex_options_for_re_module_options(Rest, [$x|Acc]); -mongo_regex_options_for_re_module_options([multiline|Rest], Acc) -> - mongo_regex_options_for_re_module_options(Rest, [$m|Acc]). - -% Boss and MongoDB have a different conventions to id attributes (id vs. '_id'). --spec attr_value(atom()|string(), proplist(atom()|string(),string())) -> maybe(string()). - -attr_value(id, MongoDoc) -> - proplists:get_value('_id', MongoDoc); -attr_value(AttrName, MongoDoc) -> - proplists:get_value(AttrName, MongoDoc). - -% Id conversions -pack_id(BossId) -> - try - [_, MongoId] = string:tokens(BossId, "-"), - {hex2dec(MongoId)} - catch - Error:Reason -> - error_logger:warning_msg("Error parsing Boss record id: ~p:~p~n", - [Error, Reason]), - [] - end. - --spec unpack_id(atom() | maybe_improper_list(),'undefined' | tuple()) -> 'undefined' | string(). -unpack_id(_Type, undefined) -> - undefined; -unpack_id(Type, MongoId) -> - lists:concat([Type, "-", binary_to_list(dec2hex(element(1, MongoId)))]). - - -% Value conversions - -pack_value({{_, _, _}, {_, _, _}} = Val) -> - datetime_to_now(Val); -pack_value([]) -> <<"">>; -pack_value(V) when is_binary(V) -> pack_value(binary_to_list(V)); -pack_value([H|T]) when is_integer(H) -> list_to_binary([H|T]); -pack_value({integers, List}) -> List; -pack_value(V) -> V. - -unpack_value(_AttrName, [H|T], _ValueType) when is_integer(H) -> - {integers, [H|T]}; -unpack_value(_AttrName, {_, _, _} = Value, datetime) -> - calendar:now_to_datetime(Value); -unpack_value(AttrName, Value, ValueType) -> - case is_id_attr(AttrName) and (Value =/= "") of - true -> - IdType = id_type_from_foreign_key(AttrName), - unpack_id(IdType, Value); - false -> - boss_record_lib:convert_value_to_type(Value, ValueType) - end. - -id_type_from_foreign_key(ForeignKey) -> - Tokens = string:tokens(atom_to_list(ForeignKey), "_"), - NameTokens = lists:filter(fun(Token) -> Token =/= "id" end, - Tokens), - string:join(NameTokens, "_"). - - -% Operators - -boss_to_mongo_op('not_equals') -> '$ne'; -boss_to_mongo_op('gt') -> '$gt'; -boss_to_mongo_op('ge') -> '$gte'; -boss_to_mongo_op('lt') -> '$lt'; -boss_to_mongo_op('le') -> '$lte'; -boss_to_mongo_op('in') -> '$in'; -boss_to_mongo_op('not_in') -> '$nin'. - - -% Sort clauses - -pack_sort_order(ascending) -> 1; -pack_sort_order(descending) -> -1. - - -%% -%% Generic data structure conversions -%% - -% The mongodb driver uses "associative tuples" which look like: -% {key1, Value1, key2, Value2} -tuple_to_proplist(Tuple) -> - List = tuple_to_list(Tuple), - Ret = lists:reverse(list_to_proplist(List, [])), - Ret. - -proplist_to_tuple(PropList) -> - ListOfLists = lists:reverse([[K,V]||{K,V} <- PropList]), - list_to_tuple(lists:foldl( - fun([K, V], Acc) -> - [K,V|Acc] - end, [], ListOfLists)). - -list_to_proplist([], Acc) -> Acc; -list_to_proplist([K,V|T], Acc) -> - list_to_proplist(T, [{K, V}|Acc]). - -datetime_to_now(DateTime) -> - GSeconds = calendar:datetime_to_gregorian_seconds(DateTime), - ESeconds = GSeconds - ?GREGORIAN_SECONDS_1970, - {ESeconds div 1000000, ESeconds rem 1000000, 0}. - - -%% -%% Decimal to hexadecimal conversion -%% -%% Functions below copied from emongo -%% -%% Copyright (c) 2009 Jacob Vorreuter -%% Jacob Perkins -%% Belyaev Dmitry -%% François de Metz -%% - -dec2hex(Dec) -> - dec2hex(<<>>, Dec). - -dec2hex(N, <>) -> - dec2hex(<>, Rem); -dec2hex(N,<<>>) -> - N. - -hex2dec(Hex) when is_list(Hex) -> - hex2dec(list_to_binary(Hex)); - -hex2dec(Hex) -> - hex2dec(<<>>, Hex). - -hex2dec(N,<>) -> - hex2dec(<>, Rem); -hex2dec(N,<<>>) -> - N. - -dec0($a) -> 10; -dec0($b) -> 11; -dec0($c) -> 12; -dec0($d) -> 13; -dec0($e) -> 14; -dec0($f) -> 15; -dec0(X) -> X - $0. - -hex0(10) -> $a; -hex0(11) -> $b; -hex0(12) -> $c; -hex0(13) -> $d; -hex0(14) -> $e; -hex0(15) -> $f; -hex0(I) -> $0 + I. diff --git a/src/db_adapters/boss_db_adapter_mysql.erl b/src/db_adapters/boss_db_adapter_mysql.erl index 36ccd67d..972676e0 100644 --- a/src/db_adapters/boss_db_adapter_mysql.erl +++ b/src/db_adapters/boss_db_adapter_mysql.erl @@ -1,15 +1,14 @@ -module(boss_db_adapter_mysql). -behaviour(boss_db_adapter). --export([init/1, terminate/1, start/1, stop/0, find/2, find/7, find_by_sql/4]). +-export([init/1, terminate/1, find/2, find/7, find_by_sql/4]). -export([count/3, counter/2, incr/3, delete/2, save_record/2]). -export([push/2, pop/2, dump/1, execute/2, execute/3, transaction/2]). -export([get_migrations_table/1, migration_done/3]). --compile(export_all). -start(_) -> - ok. -stop() -> - ok. +-ifdef(TEST). +%-include_lib("eunit/include/eunit.hrl"). +-compile(export_all). +-endif. init(Options) -> DBHost = proplists:get_value(db_host, Options, "localhost"), @@ -17,10 +16,13 @@ init(Options) -> DBUsername = proplists:get_value(db_username, Options, "guest"), DBPassword = proplists:get_value(db_password, Options, ""), DBDatabase = proplists:get_value(db_database, Options, "test"), - DBIdentifier = proplists:get_value(db_shard_id, Options, boss_pool), - Encoding = utf8, - mysql_conn:start_link(DBHost, DBPort, DBUsername, DBPassword, DBDatabase, - fun(_, _, _, _) -> ok end, Encoding, DBIdentifier). + %DBIdentifier = proplists:get_value(db_shard_id, Options, boss_pool), + %Encoding = utf8, + % {ssl, [{server_name_indication, disable}, {cacertfile, "/path/to/ca.pem"}]} + mysql:start_link([{host, DBHost}, {user, DBUsername}, {port, DBPort}, + {password, DBPassword}, {database, DBDatabase}]). + %mysql_conn:start_link(DBHost, DBPort, DBUsername, DBPassword, DBDatabase, + % fun(_, _, _, _) -> ok end, Encoding, DBIdentifier). terminate(Pid) -> exit(Pid, normal). @@ -30,14 +32,14 @@ find_by_sql(Pid, Type, Sql, Parameters) when is_atom(Type), is_list(Sql), is_lis true -> Res = fetch(Pid, Sql, Parameters), case Res of - {data, MysqlRes} -> - Rows = mysql:get_result_rows(MysqlRes), - Columns = mysql:get_result_field_info(MysqlRes), + {ok, Columns, Rows} -> + %Rows = mysql:get_result_rows(MysqlRes), + %Columns = mysql:get_result_field_info(MysqlRes), lists:map(fun(Row) -> activate_record(Row, Columns, Type) end, Rows); - {error, MysqlRes} -> - {error, mysql:get_result_reason(MysqlRes)} + {error, Reason} -> + {error, Reason} end; false -> {error, {module_not_loaded, Type}} @@ -48,18 +50,18 @@ find(Pid, Id) when is_list(Id) -> {Type, TableName, IdColumn, TableId} = boss_sql_lib:infer_type_from_id(Id), Res = fetch(Pid, ["SELECT * FROM ", TableName, " WHERE ", IdColumn, " = ", pack_value(TableId)]), case Res of - {data, MysqlRes} -> - case mysql:get_result_rows(MysqlRes) of + {ok, Columns, Rows} -> + case Rows of [] -> undefined; [Row] -> - Columns = mysql:get_result_field_info(MysqlRes), + %Columns = mysql:get_result_field_info(MysqlRes), case boss_record_lib:ensure_loaded(Type) of true -> activate_record(Row, Columns, Type); false -> {error, {module_not_loaded, Type}} end end; - {error, MysqlRes} -> - {error, mysql:get_result_reason(MysqlRes)} + {error, Reason} -> + {error, Reason} end. find(Pid, Type, Conditions, Max, Skip, Sort, SortOrder) when is_atom(Type), is_list(Conditions), @@ -71,9 +73,9 @@ find(Pid, Type, Conditions, Max, Skip, Sort, SortOrder) when is_atom(Type), is_l Res = fetch(Pid, Query), case Res of - {data, MysqlRes} -> - Columns = mysql:get_result_field_info(MysqlRes), - ResultRows = mysql:get_result_rows(MysqlRes), + {ok, Columns, ResultRows} -> + %Columns = mysql:get_result_field_info(MysqlRes), + %ResultRows = mysql:get_result_rows(MysqlRes), FilteredRows = case {Max, Skip} of {all, Skip} when Skip > 0 -> lists:nthtail(Skip, ResultRows); @@ -83,8 +85,8 @@ find(Pid, Type, Conditions, Max, Skip, Sort, SortOrder) when is_atom(Type), is_l lists:map(fun(Row) -> activate_record(Row, Columns, Type) end, FilteredRows); - {error, MysqlRes} -> - {error, mysql:get_result_reason(MysqlRes)} + {error, Reason} -> + {error, Reason} end; false -> {error, {module_not_loaded, Type}} end. @@ -94,28 +96,16 @@ count(Pid, Type, Conditions) -> TableName = boss_record_lib:database_table(Type), Res = fetch(Pid, ["SELECT COUNT(*) AS count FROM ", TableName, " WHERE ", ConditionClause]), case Res of - {data, MysqlRes} -> - [[Count]] = mysql:get_result_rows(MysqlRes), - Count; - {error, MysqlRes} -> - {error, mysql:get_result_reason(MysqlRes)} + {ok, _, [[Count]]} -> Count; + {error, Reason} -> + {error, Reason} end. -table_exists(Pid, Type) -> - TableName = boss_record_lib:database_table(Type), - Res = fetch(Pid, ["SELECT 1 FROM ", TableName," LIMIT 1"]), - case Res of - {updated, _} -> - ok; - {error, MysqlRes} -> {error, mysql:get_result_reason(MysqlRes)} - end. counter(Pid, Id) when is_list(Id) -> Res = fetch(Pid, ["SELECT value FROM counters WHERE name = ", pack_value(Id)]), case Res of - {data, MysqlRes} -> - [[Value]] = mysql:get_result_rows(MysqlRes), - Value; + {ok, _, [[Value]]} -> Value; {error, _Reason} -> 0 end. @@ -123,14 +113,14 @@ incr(Pid, Id, Count) -> Res = fetch(Pid, ["UPDATE counters SET value = value + ", pack_value(Count), " WHERE name = ", pack_value(Id)]), case Res of - {updated, _} -> + {ok, _} -> counter(Pid, Id); % race condition {error, _Reason} -> Res1 = fetch(Pid, ["INSERT INTO counters (name, value) VALUES (", pack_value(Id), ", ", pack_value(Count), ")"]), case Res1 of - {updated, _} -> counter(Pid, Id); % race condition - {error, MysqlRes} -> {error, mysql:get_result_reason(MysqlRes)} + {ok, _} -> counter(Pid, Id); % race condition + {error, Reason} -> {error, Reason} end end. @@ -139,11 +129,11 @@ delete(Pid, Id) when is_list(Id) -> Res = fetch(Pid, ["DELETE FROM ", TableName, " WHERE ", IdColumn, " = ", pack_value(TableId)]), case Res of - {updated, _} -> + ok -> fetch(Pid, ["DELETE FROM counters WHERE name = ", pack_value(Id)]), ok; - {error, MysqlRes} -> {error, mysql:get_result_reason(MysqlRes)} + {error, Reason} -> {error, Reason} end. save_record(Pid, Record) when is_tuple(Record) -> @@ -151,34 +141,28 @@ save_record(Pid, Record) when is_tuple(Record) -> id -> Type = element(1, Record), Query = build_insert_query(Record), - Res = fetch(Pid, Query), + Res = fetch(Pid, Query), case Res of - {updated, _} -> - Res1 = fetch(Pid, "SELECT last_insert_id()"), - case Res1 of - {data, MysqlRes} -> - [[Id]] = mysql:get_result_rows(MysqlRes), - {ok, Record:set(id, lists:concat([Type, "-", integer_to_list(Id)]))}; - {error, MysqlRes} -> - {error, mysql:get_result_reason(MysqlRes)} - end; - {error, MysqlRes} -> {error, mysql:get_result_reason(MysqlRes)} + ok -> + Id = mysql:insert_id(Pid), + {ok, Record:set(id, lists:concat([Type, "-", integer_to_list(Id)]))}; + {error, Reason} -> {error, Reason} end; Identifier when is_integer(Identifier) -> Type = element(1, Record), Query = build_insert_query(Record), Res = fetch(Pid, Query), case Res of - {updated, _} -> + ok -> {ok, Record:set(id, lists:concat([Type, "-", integer_to_list(Identifier)]))}; - {error, MysqlRes} -> {error, mysql:get_result_reason(MysqlRes)} + {error, Reason} -> {error, Reason} end; Defined when is_list(Defined) -> Query = build_update_query(Record), Res = fetch(Pid, Query), case Res of - {updated, _} -> {ok, Record}; - {error, MysqlRes} -> {error, mysql:get_result_reason(MysqlRes)} + ok -> {ok, Record}; + {error, Reason} -> {error, Reason} end end. @@ -211,7 +195,7 @@ do_transaction(Pid, TransactionFun) when is_function(TransactionFun) -> case do_begin(Pid, self()) of {error, _} = Err -> {aborted, Err}; - {updated,{mysql_result,[],[],0,0,[],0,[]}} -> + ok -> % {mysql_result,[],[],0,0,[],0,[]} case catch TransactionFun() of error = Err -> do_rollback(Pid, self()), @@ -265,6 +249,7 @@ activate_record(Record, Metadata, Type) -> apply(Type, new, lists:map(fun (id) -> + DBColumn = proplists:get_value('id', AttributeColumns), Index = keyindex(list_to_binary(DBColumn), 2, Metadata), atom_to_list(Type) ++ "-" ++ integer_to_list(lists:nth(Index, Record)); @@ -280,8 +265,8 @@ activate_record(Record, Metadata, Type) -> end end, boss_record_lib:attribute_names(Type))). -keyindex(Key, N, TupleList) -> - keyindex(Key, N, TupleList, 1). +keyindex(Key, N, List) -> + keyindex(Key, N, lists:map(fun(X) -> {X,X} end, List), 1). keyindex(_Key, _N, [], _Index) -> undefined; @@ -468,7 +453,7 @@ pack_value(undefined) -> pack_value(V) when is_binary(V) -> pack_value(binary_to_list(V)); pack_value(V) when is_list(V) -> - mysql:encode(V); + quote(V); pack_value({_, _, _} = Val) -> pack_date(Val); pack_value({{_, _, _}, {_, _, _}} = Val) -> @@ -482,12 +467,36 @@ pack_value(true) -> pack_value(false) -> "FALSE". +quote(String) when is_list(String) -> + [39 | lists:reverse([39 | quote(String, [])])]; %% 39 is $' +quote(Bin) when is_binary(Bin) -> + list_to_binary(quote(binary_to_list(Bin))). + +quote([], Acc) -> + Acc; +quote([0 | Rest], Acc) -> + quote(Rest, [$0, $\\ | Acc]); +quote([10 | Rest], Acc) -> + quote(Rest, [$n, $\\ | Acc]); +quote([13 | Rest], Acc) -> + quote(Rest, [$r, $\\ | Acc]); +quote([$\\ | Rest], Acc) -> + quote(Rest, [$\\ , $\\ | Acc]); +quote([39 | Rest], Acc) -> %% 39 is $' + quote(Rest, [39, $\\ | Acc]); %% 39 is $' +quote([34 | Rest], Acc) -> %% 34 is $" + quote(Rest, [34, $\\ | Acc]); %% 34 is $" +quote([26 | Rest], Acc) -> + quote(Rest, [$Z, $\\ | Acc]); +quote([C | Rest], Acc) -> + quote(Rest, [C | Acc]). + fetch(Pid, Query) -> - _ = lager:info("Query ~s", [Query]), - Res = mysql_conn:fetch(Pid, [Query], self()), + logger:info("Query ~s", [Query]), + Res = mysql:query(Pid, [Query]), _ = case Res of - {error, MysqlRes} -> - _ = lager:error("SQL Error: ~p",[mysql:get_result_reason(MysqlRes)]); + {error, Reason} -> + logger:error("SQL Error: ~p",[Reason]); _ -> ok end, Res. @@ -497,7 +506,7 @@ fetch(Pid, Query, Parameters) -> fetch(Pid, Sql). replace_parameters([$$, X, Y | Rest], Parameters) when X >= $1, X =< $9, Y >= $0, Y =< $9 -> - Position = (X-$0)*10 + (Y-$0), + Position = (X-$0)*10 + (Y-$0), [lookup_single_parameter(Position, Parameters) | replace_parameters(Rest, Parameters)]; replace_parameters([$$, X | Rest], Parameters) when X >= $1, X =< $9 -> Position = X-$0, diff --git a/src/db_adapters/boss_db_adapter_pgsql.erl b/src/db_adapters/boss_db_adapter_pgsql.erl index 5b1fb136..5efe27ee 100644 --- a/src/db_adapters/boss_db_adapter_pgsql.erl +++ b/src/db_adapters/boss_db_adapter_pgsql.erl @@ -1,20 +1,19 @@ -module(boss_db_adapter_pgsql). -behaviour(boss_db_adapter). --export([init/1, terminate/1, start/1, stop/0, find/2, find/7, find_by_sql/4]). + +-export([init/1, terminate/1, find/2, find/7, find_by_sql/4]). -export([count/3, counter/2, incr/3, delete/2, save_record/2]). -export([push/2, pop/2, dump/1, execute/2, execute/3, transaction/2, create_table/3, table_exists/2]). -export([get_migrations_table/1, migration_done/3]). + +%% Export all functions when testing +-ifdef(TEST). -compile(export_all). -%-type date_time() ::{{1970..3000,calendar:month(),calne},{pos_integer(),pos_integer(),pos_integer()|float()}}. +-endif. + -type date_time() :: calendar:datetime1970(). -type sql_param_value() :: string()|number()|binary()|boolean(). -export_type([sql_param_value/0]). --compile(export_all). -start(_) -> - ok. - -stop() -> - ok. init(Options) -> DBHost = proplists:get_value(db_host, Options, "localhost"), @@ -136,7 +135,7 @@ delete(Conn, Id) when is_list(Id) -> save_record(Conn, Record) when is_tuple(Record) -> RecordId = Record:id(), - _ = lager:notice("Saving Record ~p~n", [Record]), + logger:notice("Saving Record ~p~n", [Record]), case RecordId of id -> Record1 = maybe_populate_id_value(Record), diff --git a/src/db_adapters/boss_db_adapter_riak.erl b/src/db_adapters/boss_db_adapter_riak.erl index 9af298a1..fb239b00 100644 --- a/src/db_adapters/boss_db_adapter_riak.erl +++ b/src/db_adapters/boss_db_adapter_riak.erl @@ -1,19 +1,13 @@ -module(boss_db_adapter_riak). -behaviour(boss_db_adapter). --export([init/1, terminate/1, start/1, stop/0, find/2, find/7]). +-export([init/1, terminate/1, find/2, find/7]). -export([count/3, counter/2, incr/2, incr/3, delete/2, save_record/2]). -export([push/2, pop/2]). --define(LOG(Name, Value), lager:debug("DEBUG: ~s: ~p~n", [Name, Value])). +-define(LOG(Name, Value), logger:debug("DEBUG: ~s: ~p~n", [Name, Value])). -define(HUGE_INT, 1000 * 1000 * 1000 * 1000). -start(_) -> - ok. - -stop() -> - ok. - init(Options) -> Host = proplists:get_value(db_host, Options, "localhost"), Port = proplists:get_value(db_port, Options, 8087), diff --git a/src/db_adapters/boss_db_adapter_tyrant.erl b/src/db_adapters/boss_db_adapter_tyrant.erl deleted file mode 100644 index 5c63a66d..00000000 --- a/src/db_adapters/boss_db_adapter_tyrant.erl +++ /dev/null @@ -1,214 +0,0 @@ --module(boss_db_adapter_tyrant). --behaviour(boss_db_adapter). --export([init/1, terminate/1, start/1, stop/0, find/2, find/7]). --export([count/3, counter/2, incr/3, delete/2, save_record/2]). - --define(TRILLION, (1000 * 1000 * 1000 * 1000)). - -start(_) -> - ok. - -stop() -> - ok. - -init(Options) -> - Host = proplists:get_value(db_host, Options, "localhost"), - Port = proplists:get_value(db_port, Options, 1978), - DBConfigure = proplists:get_value(db_configure, Options, []), - PrincipeOptions = [{hostname, Host}, {port, Port} | DBConfigure], - principe:connect(PrincipeOptions). - -terminate(Conn) -> - gen_tcp:close(Conn). - -find(Conn, Id) when is_list(Id) -> - Type = infer_type_from_id(Id), - case principe_table:get(Conn, list_to_binary(Id)) of - Record when is_list(Record) -> - case boss_record_lib:ensure_loaded(Type) of - true -> activate_record(Record, Type); - false -> {error, {module_not_loaded, Type}} - end; - {error, invalid_operation} -> - undefined; - {error, Reason} -> - {error, Reason} - end. - -find(Conn, Type, Conditions, Max, Skip, Sort, SortOrder) when is_atom(Type), is_list(Conditions), - is_integer(Max) orelse Max =:= all, - is_integer(Skip), is_atom(Sort), is_atom(SortOrder) -> - case boss_record_lib:ensure_loaded(Type) of - true -> - AttributeTypes = boss_record_lib:attribute_types(Type), - TypedSortOrder = case {SortOrder, proplists:get_value(Sort, AttributeTypes)} of - {ascending, SortType} when SortType =:= float; SortType =:= integer; SortType =:= datetime; SortType =:= timestamp -> num_ascending; - {descending, SortType} when SortType =:= float; SortType =:= integer; SortType =:= datetime; SortType =:= timestamp -> num_descending; - {ascending, _} -> str_ascending; - {descending, _} -> str_descending - end, - Query = build_query(Type, Conditions, Max, Skip, Sort, TypedSortOrder), - ResultRows = principe_table:mget(Conn, principe_table:search(Conn, Query)), - FilteredRows = case {Max, Skip} of - {all, Skip} when Skip > 0 -> - lists:nthtail(Skip, ResultRows); - _ -> - ResultRows - end, - lists:map(fun({_Id, Record}) -> activate_record(Record, Type) end, FilteredRows); - false -> - [] - end. - -count(Conn, Type, Conditions) -> - principe_table:searchcount(Conn, build_conditions(Type, Conditions)). - -counter(Conn, Id) when is_list(Id) -> - case principe_table:get(Conn, list_to_binary(Id)) of - Record when is_list(Record) -> - list_to_integer(binary_to_list( - proplists:get_value(<<"_num">>, Record, <<"0">>))); - {error, _Reason} -> 0 - end. - -incr(Conn, Id, Count) when is_list(Id) -> - principe_table:addint(Conn, list_to_binary(Id), Count). - -delete(Conn, Id) when is_list(Id) -> - principe_table:out(Conn, list_to_binary(Id)). - -save_record(Conn, Record) when is_tuple(Record) -> - Type = element(1, Record), - Id = case Record:id() of - id -> - atom_to_list(Type) ++ "-" ++ binary_to_list(principe_table:genuid(Conn)); - Defined when is_list(Defined) -> - Defined - end, - RecordWithId = Record:set(id, Id), - PackedRecord = pack_record(RecordWithId, Type), - - Result = principe_table:put(Conn, list_to_binary(Id), PackedRecord), - case Result of - ok -> {ok, RecordWithId}; - {error, Error} -> {error, [Error]} - end. - -% internal - -pack_record(RecordWithId, Type) -> - Columns = lists:map(fun - (Attr) -> - Val = RecordWithId:Attr(), - {attribute_to_colname(Attr), pack_value(Val)} - end, RecordWithId:attribute_names()), - [{attribute_to_colname('_type'), list_to_binary(atom_to_list(Type))}|Columns]. - -infer_type_from_id(Id) when is_list(Id) -> - list_to_atom(hd(string:tokens(Id, "-"))). - -activate_record(Record, Type) -> - AttributeTypes = boss_record_lib:attribute_types(Type), - apply(Type, new, lists:map(fun - (Key) -> - Val = proplists:get_value(attribute_to_colname(Key), Record, <<"">>), - AttrType = proplists:get_value(Key, AttributeTypes, string), - case AttrType of - datetime -> unpack_datetime(Val); - timestamp -> DateTime = unpack_datetime(Val), - boss_record_lib:convert_value_to_type(DateTime, timestamp); - float -> list_to_integer(binary_to_list(Val)) / ?TRILLION; - _ -> boss_record_lib:convert_value_to_type(Val, AttrType) - end - end, boss_record_lib:attribute_names(Type))). - -attribute_to_colname(Attribute) -> - list_to_binary(atom_to_list(Attribute)). - -build_query(Type, Conditions, Max, Skip, Sort, SortOrder) -> - Query = build_conditions(Type, Conditions), - Query1 = apply_limit(Query, Max, Skip), - principe_table:query_order(Query1, atom_to_list(Sort), SortOrder). - -apply_limit(Query, all, _) -> - Query; -apply_limit(Query, Max, Skip) -> - principe_table:query_limit(Query, Max, Skip). - -build_conditions(Type, Conditions) -> - build_conditions1([{'_type', 'equals', atom_to_list(Type)}|Conditions], []). - -build_conditions1([], Acc) -> - Acc; -build_conditions1([{Key, 'equals', Value}|Rest], Acc) -> - build_conditions1(Rest, add_cond(Acc, Key, str_eq, pack_value(Value))); -build_conditions1([{Key, 'not_equals', Value}|Rest], Acc) -> - build_conditions1(Rest, add_cond(Acc, Key, {no, str_eq}, pack_value(Value))); -build_conditions1([{Key, 'in', Value}|Rest], Acc) when is_list(Value) -> - PackedValues = pack_tokens(Value), - build_conditions1(Rest, add_cond(Acc, Key, str_in_list, PackedValues)); -build_conditions1([{Key, 'not_in', Value}|Rest], Acc) when is_list(Value) -> - PackedValues = pack_tokens(Value), - build_conditions1(Rest, add_cond(Acc, Key, {no, str_in_list}, PackedValues)); -build_conditions1([{Key, 'in', {Min, Max}}|Rest], Acc) when Max >= Min -> - PackedValues = pack_tokens([Min, Max]), - build_conditions1(Rest, add_cond(Acc, Key, num_between, PackedValues)); -build_conditions1([{Key, 'not_in', {Min, Max}}|Rest], Acc) when Max >= Min -> - PackedValues = pack_tokens([Min, Max]), - build_conditions1(Rest, add_cond(Acc, Key, {no, num_between}, PackedValues)); -build_conditions1([{Key, 'gt', Value}|Rest], Acc) -> - build_conditions1(Rest, add_cond(Acc, Key, num_gt, pack_value(Value))); -build_conditions1([{Key, 'lt', Value}|Rest], Acc) -> - build_conditions1(Rest, add_cond(Acc, Key, num_lt, pack_value(Value))); -build_conditions1([{Key, 'ge', Value}|Rest], Acc) -> - build_conditions1(Rest, add_cond(Acc, Key, num_ge, pack_value(Value))); -build_conditions1([{Key, 'le', Value}|Rest], Acc) -> - build_conditions1(Rest, add_cond(Acc, Key, num_le, pack_value(Value))); -build_conditions1([{Key, 'matches', Value}|Rest], Acc) -> - build_conditions1(Rest, add_cond(Acc, Key, str_regex, pack_value(Value))); -build_conditions1([{Key, 'not_matches', Value}|Rest], Acc) -> - build_conditions1(Rest, add_cond(Acc, Key, {no, str_regex}, pack_value(Value))); -build_conditions1([{Key, 'contains', Value}|Rest], Acc) -> - build_conditions1(Rest, add_cond(Acc, Key, str_and, pack_value(Value))); -build_conditions1([{Key, 'not_contains', Value}|Rest], Acc) -> - build_conditions1(Rest, add_cond(Acc, Key, {no, str_and}, pack_value(Value))); -build_conditions1([{Key, 'contains_all', Values}|Rest], Acc) when is_list(Values) -> - build_conditions1(Rest, add_cond(Acc, Key, str_and, pack_tokens(Values))); -build_conditions1([{Key, 'not_contains_all', Values}|Rest], Acc) when is_list(Values) -> - build_conditions1(Rest, add_cond(Acc, Key, {no, str_and}, pack_tokens(Values))); -build_conditions1([{Key, 'contains_any', Values}|Rest], Acc) when is_list(Values) -> - build_conditions1(Rest, add_cond(Acc, Key, str_or, pack_tokens(Values))); -build_conditions1([{Key, 'contains_none', Values}|Rest], Acc) when is_list(Values) -> - build_conditions1(Rest, add_cond(Acc, Key, {no, str_or}, pack_tokens(Values))). - -add_cond(Acc, Key, Op, PackedVal) -> - principe_table:query_add_condition(Acc, attribute_to_colname(Key), Op, [PackedVal]). - -pack_tokens(Tokens) -> - list_to_binary(string:join(lists:map(fun(V) -> binary_to_list(pack_value(V)) end, Tokens), " ")). - -pack_datetime({Date, Time}) -> - list_to_binary(integer_to_list(calendar:datetime_to_gregorian_seconds({Date, Time}))). - -pack_now(Now) -> pack_datetime(calendar:now_to_datetime(Now)). - -pack_value(V) when is_binary(V) -> - V; -pack_value(V) when is_list(V) -> - list_to_binary(V); -pack_value({MegaSec, Sec, MicroSec}) when is_integer(MegaSec) andalso is_integer(Sec) andalso is_integer(MicroSec) -> - pack_now({MegaSec, Sec, MicroSec}); -pack_value({{_, _, _}, {_, _, _}} = Val) -> - pack_datetime(Val); -pack_value(Val) when is_integer(Val) -> - list_to_binary(integer_to_list(Val)); -pack_value(Val) when is_float(Val) -> - list_to_binary(integer_to_list(trunc(Val * ?TRILLION))); -pack_value(true) -> - <<"1">>; -pack_value(false) -> - <<"0">>. - -unpack_datetime(<<"">>) -> calendar:gregorian_seconds_to_datetime(0); -unpack_datetime(Bin) -> calendar:universal_time_to_local_time( - calendar:gregorian_seconds_to_datetime(list_to_integer(binary_to_list(Bin)))). diff --git a/src/inflector.erl b/src/inflector.erl index 3a8d7da9..242a4589 100644 --- a/src/inflector.erl +++ b/src/inflector.erl @@ -27,11 +27,21 @@ -module(inflector). -author('Luke Galea '). --export([pluralize/1, singularize/1, camelize/1, lower_camelize/1, titleize/1, - capitalize/1, humanize/1, underscore/1, dasherize/1, tableize/1, moduleize/1, - foreign_key/1, ordinalize/1]). - --include_lib("eunit/include/eunit.hrl"). +-export([ + pluralize/1, + singularize/1, + camelize/1, + lower_camelize/1, + titleize/1, + capitalize/1, + humanize/1, + underscore/1, + dasherize/1, + tableize/1, + moduleize/1, + foreign_key/1, + ordinalize/1 + ]). %% External API singularize(Word) -> @@ -199,6 +209,9 @@ uncountables() -> "equipment" ]. +-ifdef(TEST). + +-include_lib("eunit/include/eunit.hrl"). %% Tests replace_test() -> SampleList = [ {"abc", "def"}, @@ -272,3 +285,5 @@ ordinalize_test() -> foreign_key_test() -> "message_id" = foreign_key("Message"). + +-endif. diff --git a/test/boss_db_adapter_mongodb_test.erl b/test/boss_db_adapter_mongodb_test.erl deleted file mode 100644 index a9b0ebb1..00000000 --- a/test/boss_db_adapter_mongodb_test.erl +++ /dev/null @@ -1,99 +0,0 @@ --module(boss_db_adapter_mongodb_test). --include_lib("proper/include/proper.hrl"). --include_lib("eunit/include/eunit.hrl"). --define(T_MODULE, boss_db_adapter_mongodb). - -hex0_test() -> - ?assert(proper:check_spec({?T_MODULE,hex0, 1}, - [{to_file, user}])), - ok. - -dec0_test() -> - ?assert(proper:check_spec({?T_MODULE,dec0, 1}, - [{to_file, user}])), - ok. - -prolist_test() -> - ?assert(proper:check_spec({?T_MODULE,proplist_to_tuple, 1}, - [{to_file, user}])), - ?assert(proper:quickcheck(prop_tuple_proplist_conversion(), - [{to_file, user}])), - ok. - -%% build_conditions2_test() -> -%% ?assert(proper:check_spec({?T_MODULE, build_conditions2, 3}, -%% [{to_file, user}])), -%% ok. - --type proplist() :: [{any(), any()}]. -prop_tuple_proplist_conversion() -> - ?FORALL(Proplist, - proplist(), - begin - Tuple = ?T_MODULE:proplist_to_tuple(Proplist), - NProplist = ?T_MODULE:tuple_to_proplist(Tuple), - NProplist =:= Proplist - end). - -pack_sort_order_test() -> - ?assert(proper:check_spec({?T_MODULE,pack_sort_order, 1}, - [{to_file, user}])), - ok. - -boss_to_mongo_op_test() -> - ?assert(proper:check_spec({?T_MODULE,boss_to_mongo_op, 1}, - [{to_file, user}])), - ok. - - - -attr_value_test() -> - ?assert(proper:check_spec({?T_MODULE,attr_value, 2}, - [{to_file, user}])), - ok. - - -prop_pack_value_binary() -> - ?FORALL(Input, - binary(), - begin - Result = ?T_MODULE:pack_value(Input), - is_binary(Result) - end). - -prop_pack_value_integer_list() -> - ?FORALL(Integers, - list(integer()), - begin - Result = ?T_MODULE:pack_value({integers, Integers}), - Result =:= Integers - end). - -pack_value_test() -> - ?assert(proper:quickcheck(prop_pack_value_integer_list(), - [{to_file, user}])), - - ?assert(proper:quickcheck(prop_pack_value_binary(), - [{to_file, user}])), - - ok. - -%% unpack_id_test() -> -%% ?assert(proper:check_spec({boss_db_adapter_mongodb,unpack_id, 2}, -%% [{to_file, user}])), -%% ok. -%% multiple_where_clauses_string_test() -> -%% ?assert(proper:quickcheck(prop_multiple_where_cluases_string(), -%% [{to_file,user}])), -%% ok. - -%% prop_multiple_where_cluases_string() -> -%% ?FORALL( ValueList, -%% ?SIZED(Size, words:word(Size)), -%% begin -%% Key = "Test", -%% Result = ?T_MODULE:multiple_where_clauses_string("~p=~p", -%% Key, ValueList,"and"), -%% ?debugVal(Result), -%% is_list(Result) -%% end). diff --git a/test/boss_db_adapter_mysql_otp_test.erl b/test/boss_db_adapter_mysql_otp_test.erl new file mode 100644 index 00000000..821c9553 --- /dev/null +++ b/test/boss_db_adapter_mysql_otp_test.erl @@ -0,0 +1,156 @@ +-module(boss_db_adapter_mysql_otp_test). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("pmod_transform/include/pmod.hrl"). +-compile(export_all). + +-define(test(Desc, F), {setup, + fun setup/0, + fun cleanup/1, + fun(State) -> + case State of + {ok, _} -> {Desc, F}; + _ -> ?debugFmt("MYSQL TEST NOT EXECUTED: ~p", [State]) + end + end}). + +setup() -> + + MysqlHost = os:getenv("MYSQL_HOST", "127.0.0.1"), + MysqlPort = os:getenv("MYSQL_PORT", "6033"), + MysqlUser = os:getenv("MYSQL_USER", "test"), + MysqlPassword = os:getenv("MYSQL_PASSWORD", "test"), + MysqlDbName = os:getenv("MYSQL_TEST_DBNAME", "test"), + + + if + length(MysqlUser) =:= 0 -> + ?debugFmt("Skip mysql otp adapter tests", []), + skip_case; + true -> + {ok, developer} = boss_record_compiler:compile("test/developer.erl"), + + DBOptions = [ + {adapter, mysql}, + {db_host, MysqlHost}, + {db_port, list_to_integer(MysqlPort)}, + {db_username, MysqlUser}, + {db_password, MysqlPassword}, + {db_database, MysqlDbName}, + {db_configure, []}, + {db_ssl, false}, % for now pgsql only + {shards, []}, + {cache_enable, false}, + {cache_exp_time, 0}, + + {size, 3}, % the size of the connection pool - defaults to 5 + {max_overflow, 3} % the maximum number of temporary extra workers that can be created past the `size' just above - defaults to 10 + %% the sum size + max_overflow effectively controls how many concurrent mysql queries can run + ], + DbServerState = boss_db:start(DBOptions), + boss_news:start(), + ?debugFmt("DbServerState = ~p", [DbServerState]), + + DbServerState + end. + +cleanup(_) -> + ok. + +create_database() -> + ok = boss_db:execute("DROP TABLE IF EXISTS developers"), + ok = boss_db:execute("create table developers( id bigint auto_increment primary key, name varchar(20), country varchar(10), created_at datetime )"), + ok. + +delete_all() -> + ok = boss_db:execute("delete from developers"). + +t_test_() -> + + ?test("test raw query", [ + ?_test(create_database()), + ?_test(test_raw_sql()), + ?_test(test_find_model_by_sql()), + ?_test(test_new_model()), + ?_test(test_find_model()), + ?_test(test_count_model()), + ?_test(test_delete_model()), + ?_test(test_transaction()), + ?_test(test_transaction_error()), + ?_test(test_count_model_by_condition()) + ]). + +test_raw_sql() -> + delete_all(), + ok = boss_db:execute("insert developers (name, country) values ('Pedro', 'Brazil')"), + {ok,[<<"id">>, <<"name">>,<<"country">>, <<"created_at">>],[[1, <<"Pedro">>,<<"Brazil">>, null]]} = boss_db:execute("select * from developers where name = 'Pedro'"), + ok. + +test_find_model_by_sql() -> + delete_all(), + ok = boss_db:execute("insert into developers (name, country) values ('Jonas', 'Brazil')"), + [{developer,_,"Jonas","Brazil", _}] = boss_db:find_by_sql(developer, "select * from developers where name = 'Jonas'"), + ok. + +test_new_model() -> + delete_all(), + Now = calendar:local_time(), + Developer = developer:new(id, "Carlos", "Brazil", Now), + {ok, NewDeveloper} = Developer:save(), + {developer, _,"Carlos","Brazil", Now} = NewDeveloper, + ok. + +test_find_model() -> + delete_all(), + Developer = developer:new(id, "Carlos", "Brazil", calendar:local_time()), + {ok, NewDeveloper} = Developer:save(), + NewDeveloper = boss_db:find_first(developer, [{id, 'equals', NewDeveloper:id()}]), + NewDeveloper = boss_db:find_last(developer, [{id, 'equals', NewDeveloper:id()}]), + ok. + +test_count_model() -> + delete_all(), + Developer = developer:new(id, "Carlos", "Brazil", calendar:local_time()), + {ok, NewDeveloper} = Developer:save(), + 1 = boss_db:count(developer), + ok. + +test_count_model_by_condition() -> + delete_all(), + Developer = developer:new(id, "Carlos", "Brazil", calendar:local_time()), + {ok, NewDeveloper} = Developer:save(), + 1 = boss_db:count(developer, [{name, 'equals', "Carlos"}]), + 0 = boss_db:count(developer, [{name, 'equals', "Carloss"}]), + ok. + +test_delete_model() -> + delete_all(), + Developer = developer:new(id, "Carlos", "Brazil", calendar:local_time()), + {ok, NewDeveloper} = Developer:save(), + ok = boss_db:delete(NewDeveloper:id()), + 0 = boss_db:count(developer), + ok. + +test_transaction() -> + delete_all(), + {atomic, _} = boss_db:transaction(fun() -> + Developer = developer:new(id, "Carlos", "Brazil", calendar:local_time()), + {ok, _} = Developer:save(), + ok + end), + + 1 = boss_db:count(developer), + + ok. + +test_transaction_error() -> + delete_all(), + {aborted, _} = boss_db:transaction(fun() -> + Developer = developer:new(id, "Carlos", "Brazil", calendar:local_time()), + {ok, _} = Developer:save(), + OtherDeveloper = developer:new(id, "Mario", "Brazilzilzilzilzilzilzil", calendar:local_time()), + OtherDeveloper:save() % return error + end), + + 0 = boss_db:count(developer), + + ok. \ No newline at end of file diff --git a/test/boss_db_adapter_pgsql_test.erl b/test/boss_db_adapter_pgsql_test.erl index 1f151527..39b80aa6 100644 --- a/test/boss_db_adapter_pgsql_test.erl +++ b/test/boss_db_adapter_pgsql_test.erl @@ -102,7 +102,7 @@ substr_to_i(Result, S, E) -> I. pack_value_test() -> - ?assert(proper:check_spec({boss_db_adapter_pgsql, pack_value,1}, + ?assert(proper:check_spec({boss_db_adapter_pgsql, pack_value, 1}, [{to_file, user}])), ok. diff --git a/test/developer.erl b/test/developer.erl new file mode 100644 index 00000000..ae640dbe --- /dev/null +++ b/test/developer.erl @@ -0,0 +1,6 @@ +-include_lib("pmod_transform/include/pmod.hrl"). +-module(developer, [Id, Name, Country, CreatedAt]). +-compile(export_all). + +attribute_types() -> + [{name, string}, {country, string}, {created_at, datetime}].