Permalink
Browse files

BossDB is now its own project

  • Loading branch information...
0 parents commit d03607384b7961f0dd06d1067dac6553fb6645b9 Evan Miller committed Feb 4, 2012
Showing with 4,859 additions and 0 deletions.
  1. +4 −0 .gitignore
  2. +41 −0 Makefile
  3. +143 −0 README.md
  4. +5 −0 priv/test_db_config/mock.config
  5. +6 −0 priv/test_db_config/mongodb.config
  6. +10 −0 priv/test_db_config/mysql.config
  7. +10 −0 priv/test_db_config/pgsql.config
  8. +6 −0 priv/test_db_config/riak.config
  9. +5 −0 priv/test_db_config/tyrant.config
  10. +4 −0 priv/test_models/boss_db_test_model.erl
  11. +2 −0 priv/test_models/boss_db_test_parent_model.erl
  12. +22 −0 priv/test_sql/mysql.sql
  13. +22 −0 priv/test_sql/pgsql.sql
  14. BIN rebar
  15. +12 −0 rebar.config
  16. +22 −0 src/boss_cache.erl
  17. +11 −0 src/boss_cache_adapter.erl
  18. +51 −0 src/boss_cache_controller.erl
  19. +23 −0 src/boss_cache_sup.erl
  20. +193 −0 src/boss_compiler.erl
  21. +19 −0 src/boss_db.app.src
  22. +247 −0 src/boss_db.erl
  23. +12 −0 src/boss_db_adapter.erl
  24. +26 −0 src/boss_db_cache.erl
  25. +183 −0 src/boss_db_controller.erl
  26. +172 −0 src/boss_db_mock_controller.erl
  27. +22 −0 src/boss_db_mock_sup.erl
  28. +246 −0 src/boss_db_pt.erl
  29. +23 −0 src/boss_db_sup.erl
  30. +11 −0 src/boss_db_test.erl
  31. +288 −0 src/boss_db_test_app.erl
  32. +85 −0 src/boss_news.erl
  33. +306 −0 src/boss_news_controller.erl
  34. +22 −0 src/boss_news_sup.erl
  35. +37 −0 src/boss_pq.erl
  36. +6 −0 src/boss_record.erl
  37. +436 −0 src/boss_record_compiler.erl
  38. +53 −0 src/boss_record_lib.erl
  39. +34 −0 src/cache_adapters/boss_cache_adapter_memcached_bin.erl
  40. +245 −0 src/db_adapters/boss_db_adapter_mnesia.erl
  41. +67 −0 src/db_adapters/boss_db_adapter_mock.erl
  42. +449 −0 src/db_adapters/boss_db_adapter_mongodb.erl
  43. +410 −0 src/db_adapters/boss_db_adapter_mysql.erl
  44. +375 −0 src/db_adapters/boss_db_adapter_pgsql.erl
  45. +255 −0 src/db_adapters/boss_db_adapter_riak.erl
  46. +238 −0 src/db_adapters/boss_db_adapter_tyrant.erl
@@ -0,0 +1,4 @@
+ebin/*.beam
+ebin/boss_db.app
+deps
+erl_crash.dump
@@ -0,0 +1,41 @@
+
+ERL=erl
+REBAR=./rebar
+DB_CONFIG_DIR=priv/test_db_config
+
+.PHONY: deps get-deps
+
+all:
+ @$(REBAR) get-deps
+ @$(REBAR) compile
+
+boss_db:
+ @$(REBAR) compile skip_deps=true
+
+clean:
+ @$(REBAR) clean
+
+get-deps:
+ @$(REBAR) get-deps
+
+deps:
+ @$(REBAR) compile
+
+test:
+ @$(REBAR) skip_deps=true eunit
+
+test_db_mock:
+ $(ERL) -pa ebin -run boss_db_test start -config $(DB_CONFIG_DIR)/mock -noshell
+
+test_db_mysql:
+ $(ERL) -pa ebin -run boss_db_test start -config $(DB_CONFIG_DIR)/mysql -noshell
+
+test_db_pgsql:
+ $(ERL) -pa ebin -run boss_db_test start -config $(DB_CONFIG_DIR)/pgsql -noshell
+
+test_db_mongodb:
+ echo "db.boss_db_test_models.remove();"|mongo boss_test
+ $(ERL) -pa ebin -run boss_db_test start -config $(DB_CONFIG_DIR)/mongodb -noshell
+
+test_db_riak:
+ $(ERL) -pa ebin -pa deps/*/ebin -run boss_db_test start -config $(DB_CONFIG_DIR)/riak -noshell
143 README.md
@@ -0,0 +1,143 @@
+BossDB: A sharded, caching, evented ORM for Erlang
+--------------------------------------------------
+
+Complete API references
+=======================
+
+Querying: http://www.chicagoboss.org/api-db.html
+Records: http://www.chicagoboss.org/api-record.html
+BossNews: http://chicagoboss.org/api-news.html
+
+Usage
+=====
+
+ boss_db:start(DBOptions),
+ boss_cache:start(CacheOptions), % If you want cacheing with Memcached
+ boss_news:start() % If you want events
+
+ DBOptions = [
+ {adapter, mock | tyrant | riak | mysql | pgsql | mnesia | mongodb},
+ {db_host, HostName::string()},
+ {db_port, PortNumber::integer()},
+ {db_username, UserName::string()},
+ {db_password, Password::string()},
+ {shards, [
+ {db_shard_models, [ModelName::atom()]},
+ {db_shard_id, ShardId::atom()},
+ {db_host, _}, {db_port, _}, ...
+ ]},
+ {cache_enable, true | false},
+ {cache_exp_time, TTLSeconds::integer()}
+ ]
+
+ CacheOptions = [
+ {adapter, memcached_bin}, % More in the future
+ {cache_servers, [{HostName::string(), Port::integer(), Weight::integer()}]}
+ ]
+
+Introduction
+============
+
+BossDB is a compiler chain and run-time library for accessing a database via
+Erlang parameterized modules. It solves the age-old problem of retrieving
+named fields without resorting to verbosities like proplists:get_value/2 or
+dict:find/2. For example, if you want to look up a puppy by ID and print its
+name, you would write:
+
+ Puppy = boss_db:find("puppy-1"),
+ io:format("Puppy's name: ~p~n", [Puppy:name()]).
+
+Functions for accessing field names are generated automatically. All you need
+to do is create a model file and compile it with boss_record_compiler. Example:
+
+The model file, call it puppy.erl:
+
+ -module(puppy, [Id, Name, BreedId]).
+
+Then compile it like:
+
+ {ok, puppy} = boss_record_compiler:compile("puppy.erl")
+
+...and you're ready to go.
+
+BossDB supports database associations. Suppose you want to model the dog breed
+(golden retriever, poodle, etc). You would create a model file with a special
+"-has" attribute, like:
+
+ -module(breed, [Id, Name]).
+ -has({puppies, many}).
+
+Then back in puppy.erl you'd add a "-belongs_to" attribute:
+
+ -module(puppy, [Id, Name, BreedId]).
+ -belongs_to(breed).
+
+Once you've compile breed.erl with boss_record_compiler, you can print a puppy's
+associated breed like:
+
+ Breed = Puppy:breed(),
+ io:format("Puppy's breed: ~p~n", [Breed:name()]).
+
+Similarly, you could iterate over all the puppies of a particular breed:
+
+ Breed = boss_db:find("breed-47"),
+ lists:map(fun(Puppy) ->
+ io:format("Puppy: ~p~n", [Puppy:name()])
+ end, Breed:puppies())
+
+You can achieve the same thing with a database query, for example:
+
+ Puppies = boss_db:find(puppy, [{breed_id, 'equals', "breed-47"}])
+
+This is somewhat verbose. If you compile the source file with boss_compiler,
+you'll be able to write the more simple expression:
+
+ Puppies = boss_db:find(puppy, [breed_id = "breed-47"])
+
+BossDB supports many query operators; see the API references at the top.
+
+To create and save a new record, you would write:
+
+ Breed = breed:new(id, "Golden Retriever"),
+ {ok, SavedBreed} = Breed:save()
+
+You can provide validation logic by adding a validation_tests/0 function
+to your model file, e.g.
+
+ -module(breed, [Id, Name]).
+ -has({puppies, many}).
+ -export([validation_tests/0]).
+
+ validation_tests() ->
+ [{fun() -> length(Name) > 0 end,
+ "Name must not be empty!"}].
+
+If validation fails, the save/0 function will return a list of error messages
+instead of the saved record.
+
+
+Events
+======
+
+BossDB provides two kinds of model events: synchronous save hooks, and
+asynchronous notifications via BossNews. Save hooks are simple; just
+define one or more of these functions in your model file:
+
+ before_create/0 -> ok | {ok, ModifiedRecord} | {error, Reason}
+ before_update/0 -> ok | {ok, ModifiedRecord} | {error, Reason}
+ after_create/0
+ after_update/0
+ before_delete/0 -> ok | {error, Reason}
+
+BossNews is more complicated but also more powerful. It is a notification
+system that executes asynchronously, so the code that calls "save" does
+not have to wait for callbacks to complete. The central concept in BossNews
+is a "watch", which is an event observer. You can create and destroy watches
+programmatically:
+
+ {ok, WatchId} = boss_news:watch(TopicString, CallBack),
+ boss_news:cancel_watch(WatchId)
+
+You can watch individual records or collections of records for changes, which
+is handy for providing real-time notifications or alerts. For details see
+the documentation at http://www.chicagoboss.org/api-news.html
@@ -0,0 +1,5 @@
+[{boss_db_test,
+ [
+ {db_adapter, mock}
+ ]
+}].
@@ -0,0 +1,6 @@
+[{boss_db_test,
+ [
+ {db_adapter, mongodb},
+ {db_database, boss_test}
+ ]
+}].
@@ -0,0 +1,10 @@
+[{boss_db_test,
+ [
+ {db_adapter, mysql},
+ {db_host, "localhost"},
+ {db_port, 3306},
+ {db_database, "boss_test"},
+ {db_username, "boss_test"},
+ {db_password, "testpass"}
+ ]
+}].
@@ -0,0 +1,10 @@
+[{boss_db_test,
+ [
+ {db_adapter, pgsql},
+ {db_host, "localhost"},
+ {db_port, 5432},
+ {db_database, "boss_test"},
+ {db_username, "boss_test"},
+ {db_password, "testpass"}
+ ]
+}].
@@ -0,0 +1,6 @@
+[{boss_db_test,
+ [
+ {db_adapter, riak},
+ {db_host, "localhost"}
+ ]
+}].
@@ -0,0 +1,5 @@
+[{boss_db_test,
+ [
+ {db_adapter, tyrant}
+ ]
+}].
@@ -0,0 +1,4 @@
+-module(boss_db_test_model, [Id, SomeText, SomeTime, SomeBoolean, SomeInteger, SomeFloat, BossDbTestParentModelId]).
+-compile(export_all).
+
+-belongs_to(boss_db_test_parent_model).
@@ -0,0 +1,2 @@
+-module(boss_db_test_parent_model, [Id, SomeText]).
+-compile(export_all).
@@ -0,0 +1,22 @@
+DROP TABLE IF EXISTS boss_db_test_models;
+CREATE TABLE boss_db_test_models (
+ id INTEGER AUTO_INCREMENT PRIMARY KEY,
+ some_text TEXT,
+ some_time DATETIME,
+ some_boolean BOOLEAN,
+ some_integer INTEGER,
+ some_float FLOAT,
+ boss_db_test_parent_model_id INTEGER
+);
+
+DROP TABLE IF EXISTS boss_db_test_parent_models;
+CREATE TABLE boss_db_test_parent_models (
+ id INTEGER AUTO_INCREMENT PRIMARY KEY,
+ some_text TEXT
+);
+
+DROP TABLE IF EXISTS counters;
+CREATE TABLE counters (
+ name VARCHAR(255) PRIMARY KEY,
+ value INTEGER DEFAULT 0
+);
@@ -0,0 +1,22 @@
+DROP TABLE IF EXISTS boss_db_test_models;
+CREATE TABLE boss_db_test_models (
+ id SERIAL PRIMARY KEY,
+ some_text TEXT,
+ some_time TIMESTAMP,
+ some_boolean BOOLEAN,
+ some_integer INTEGER,
+ some_float FLOAT,
+ boss_db_test_parent_model_id INTEGER
+);
+
+DROP TABLE IF EXISTS boss_db_test_parent_models;
+CREATE TABLE boss_db_test_models (
+ id SERIAL PRIMARY KEY,
+ some_text TEXT
+);
+
+DROP TABLE IF EXISTS counters;
+CREATE TABLE counters (
+ name VARCHAR(255) PRIMARY KEY,
+ value INTEGER DEFAULT 0
+);
BIN rebar
Binary file not shown.
@@ -0,0 +1,12 @@
+{erl_opts, [debug_info]}.
+{deps, [
+ {aleppo, ".*", {git, "git://github.com/evanmiller/aleppo.git", {tag, "1caff84da4"}}},
+ {bson, ".*", {git, "git://github.com/mongodb/bson-erlang.git", {tag, "adce0e94ab"}}},
+ {epgsql, ".*", {git, "git://github.com/wg/epgsql.git", {tag, "1.4"}}},
+ {erlmc, ".*", {git, "git://github.com/bipthelin/erlmc.git", {tag, "0.4"}}},
+ {medici, ".*", {git, "git://github.com/evanmiller/medici.git", {branch, "rebarify"}}},
+ {mongodb, ".*", {git, "git://github.com/mongodb/mongodb-erlang.git", {tag, "e5e20a0cbd"}}},
+ {mysql, ".*", {git, "git://github.com/dizzyd/erlang-mysql-driver.git", {tag, "16cae84b5e"}}},
+ {riakc, "1.1.*", {git, "git://github.com/basho/riak-erlang-client", {tag, "aa5c64a6a04192662d9c"}}},
+ {riakpool, "0.1", {git, "git://github.com/dweldon/riakpool", {tag, "HEAD"}}}
+ ]}.
@@ -0,0 +1,22 @@
+-module(boss_cache).
+-export([start/0, start/1]).
+-export([stop/0]).
+-export([get/2, set/4, delete/2]).
+
+start() ->
+ start([{adapter, boss_cache_adapter_memcached_bin}, {cache_servers, [{"127.0.0.1", 11211, 1}]}]).
+
+start(Options) ->
+ boss_cache_sup:start_link(Options).
+
+stop() ->
+ ok.
+
+set(Prefix, Key, Val, TTL) ->
+ gen_server:call(?MODULE, {set, Prefix, Key, Val, TTL}).
+
+get(Prefix, Key) ->
+ gen_server:call(?MODULE, {get, Prefix, Key}).
+
+delete(Prefix, Key) ->
+ gen_server:call(?MODULE, {delete, Prefix, Key}).
@@ -0,0 +1,11 @@
+-module(boss_cache_adapter).
+-export([behaviour_info/1]).
+
+%% @spec behaviour_info( atom() ) -> [ {Function::atom(), Arity::integer()} ] | undefined
+behaviour_info(callbacks) ->
+ [
+ {start, 0}, {start, 1}, {stop, 1},
+ {get, 3}, {set, 5}, {delete, 3}
+ ];
+behaviour_info(_Other) ->
+ undefined.
@@ -0,0 +1,51 @@
+-module(boss_cache_controller).
+
+-behaviour(gen_server).
+
+-export([start_link/0, start_link/1]).
+
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
+
+-record(state, {
+ adapter,
+ connection
+ }).
+
+start_link() ->
+ start_link([]).
+
+start_link(Args) ->
+ gen_server:start_link({local, boss_cache}, ?MODULE, Args, []).
+
+init(Options) ->
+ AdapterName = proplists:get_value(adapter, Options, memcached_bin),
+ Adapter = list_to_atom(lists:concat(["boss_cache_adapter_", AdapterName])),
+ {ok, Conn} = Adapter:start(Options),
+ {ok, #state{ adapter = Adapter, connection = Conn }}.
+
+handle_call({get, Prefix, Key}, _From, State) ->
+ Adapter = State#state.adapter,
+ Conn = State#state.connection,
+ {reply, Adapter:get(Conn, Prefix, Key), State};
+handle_call({set, Prefix, Key, Value, TTL}, _From, State) ->
+ Adapter = State#state.adapter,
+ Conn = State#state.connection,
+ {reply, Adapter:set(Conn, Prefix, Key, Value, TTL), State};
+handle_call({delete, Prefix, Key}, _From, State) ->
+ Adapter = State#state.adapter,
+ Conn = State#state.connection,
+ {reply, Adapter:delete(Conn, Prefix, Key), State}.
+
+handle_cast(_Request, State) ->
+ {noreply, State}.
+
+terminate(_Reason, State) ->
+ Adapter = State#state.adapter,
+ Conn = State#state.connection,
+ Adapter:stop(Conn).
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+handle_info(_Info, State) ->
+ {noreply, State}.
Oops, something went wrong.

0 comments on commit d036073

Please sign in to comment.