rethinker 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +10 -0
  3. data/README.md +49 -0
  4. data/USAGE.rb.md +99 -0
  5. data/lib/rethinker.rb +38 -0
  6. data/lib/rethinker/autoload.rb +14 -0
  7. data/lib/rethinker/connection.rb +49 -0
  8. data/lib/rethinker/criterion.rb +32 -0
  9. data/lib/rethinker/database.rb +38 -0
  10. data/lib/rethinker/document.rb +8 -0
  11. data/lib/rethinker/document/attributes.rb +117 -0
  12. data/lib/rethinker/document/core.rb +37 -0
  13. data/lib/rethinker/document/dynamic_attributes.rb +12 -0
  14. data/lib/rethinker/document/id.rb +54 -0
  15. data/lib/rethinker/document/injection_layer.rb +11 -0
  16. data/lib/rethinker/document/persistence.rb +79 -0
  17. data/lib/rethinker/document/polymorphic.rb +37 -0
  18. data/lib/rethinker/document/relation.rb +34 -0
  19. data/lib/rethinker/document/selection.rb +49 -0
  20. data/lib/rethinker/document/serialization.rb +41 -0
  21. data/lib/rethinker/document/timestamps.rb +18 -0
  22. data/lib/rethinker/document/validation.rb +62 -0
  23. data/lib/rethinker/error.rb +6 -0
  24. data/lib/rethinker/query_runner.rb +32 -0
  25. data/lib/rethinker/query_runner/connection.rb +17 -0
  26. data/lib/rethinker/query_runner/database_on_demand.rb +16 -0
  27. data/lib/rethinker/query_runner/driver.rb +10 -0
  28. data/lib/rethinker/query_runner/selection.rb +8 -0
  29. data/lib/rethinker/query_runner/table_on_demand.rb +12 -0
  30. data/lib/rethinker/query_runner/write_error.rb +34 -0
  31. data/lib/rethinker/railtie.rb +16 -0
  32. data/lib/rethinker/railtie/database.rake +37 -0
  33. data/lib/rethinker/relation.rb +6 -0
  34. data/lib/rethinker/relation/belongs_to.rb +36 -0
  35. data/lib/rethinker/relation/has_many.rb +30 -0
  36. data/lib/rethinker/relation/has_many/selection.rb +27 -0
  37. data/lib/rethinker/selection.rb +6 -0
  38. data/lib/rethinker/selection/core.rb +39 -0
  39. data/lib/rethinker/selection/count.rb +13 -0
  40. data/lib/rethinker/selection/delete.rb +9 -0
  41. data/lib/rethinker/selection/enumerable.rb +22 -0
  42. data/lib/rethinker/selection/first.rb +20 -0
  43. data/lib/rethinker/selection/inc.rb +12 -0
  44. data/lib/rethinker/selection/limit.rb +15 -0
  45. data/lib/rethinker/selection/order_by.rb +41 -0
  46. data/lib/rethinker/selection/scope.rb +11 -0
  47. data/lib/rethinker/selection/update.rb +6 -0
  48. data/lib/rethinker/selection/where.rb +23 -0
  49. data/lib/rethinker/version.rb +3 -0
  50. metadata +140 -0
@@ -0,0 +1,6 @@
1
+ module Rethinker::Error
2
+ class Connection < StandardError; end
3
+ class DocumentNotFound < StandardError; end
4
+ class DocumentInvalid < StandardError; end
5
+ class DocumentNotSaved < StandardError; end
6
+ end
@@ -0,0 +1,32 @@
1
+ require 'middleware'
2
+
3
+ module Rethinker::QueryRunner
4
+ extend Rethinker::Autoload
5
+
6
+ class Middleware
7
+ def initialize(runner)
8
+ @runner = runner
9
+ end
10
+ end
11
+
12
+ autoload :Driver, :DatabaseOnDemand, :TableOnDemand, :WriteError,
13
+ :Connection, :Selection
14
+
15
+ class << self
16
+ attr_accessor :stack
17
+
18
+ def run(options={}, &block)
19
+ stack.call(:query => yield, :options => options)
20
+ end
21
+ end
22
+
23
+ # thread-safe, since require() is ran with a mutex.
24
+ self.stack = ::Middleware::Builder.new do
25
+ use Selection
26
+ use Connection
27
+ use WriteError
28
+ use TableOnDemand
29
+ use DatabaseOnDemand
30
+ use Driver
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ class Rethinker::QueryRunner::Connection < Rethinker::QueryRunner::Middleware
2
+ def call(env)
3
+ @runner.call(env)
4
+ rescue RuntimeError, Rethinker::Error::DocumentNotSaved => e
5
+ if e.message =~ /cannot perform (read|write): lost contact with master/
6
+ env[:connection_retries] ||= 0
7
+ # TODO sleep in between? timing out should be time based?
8
+
9
+ # XXX Possibly dangerous, as we could reexecute a non idempotent operation
10
+ # Check the semantics of the db
11
+
12
+ # TODO Unit test
13
+ retry if (env[:connection_retries] += 1) < 10
14
+ end
15
+ raise e
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ class Rethinker::QueryRunner::DatabaseOnDemand < Rethinker::QueryRunner::Middleware
2
+ def call(env)
3
+ @runner.call(env)
4
+ rescue RuntimeError => e
5
+ if e.message =~ /^Database `(.+)` does not exist\.$/
6
+ # RethinkDB may return an FIND_DB not found immediately
7
+ # after having created the new database, Be patient.
8
+ # TODO Unit test that thing
9
+ # Also, should we be counter based, or time based for the timeout ?
10
+ Rethinker.db_create $1 unless env[:db_find_retries]
11
+ env[:db_find_retries] ||= 0
12
+ retry if (env[:db_find_retries] += 1) < 10
13
+ end
14
+ raise e
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ class Rethinker::QueryRunner::Driver < Rethinker::QueryRunner::Middleware
2
+ def call(env)
3
+ # TODO have a logger
4
+ puts env[:query].inspect if ENV['DEBUG']
5
+ env[:query].run(Rethinker.connection, env[:options])
6
+ rescue NoMethodError => e
7
+ raise "Rethinker is not connected to a RethinkDB instance" unless Rethinker.connection
8
+ raise e
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ class Rethinker::QueryRunner::Selection < Rethinker::QueryRunner::Middleware
2
+ def call(env)
3
+ if env[:query].is_a? Rethinker::Selection
4
+ env[:selection], env[:query] = env[:query], env[:query].query
5
+ end
6
+ @runner.call(env)
7
+ end
8
+ end
@@ -0,0 +1,12 @@
1
+ class Rethinker::QueryRunner::TableOnDemand < Rethinker::QueryRunner::Middleware
2
+ def call(env)
3
+ @runner.call(env)
4
+ rescue RuntimeError => e
5
+ if e.message =~ /^Table `(.+)` does not exist\.$/
6
+ # TODO Lookup the Model, and get the primary key name
7
+ Rethinker.table_create $1
8
+ retry
9
+ end
10
+ raise e
11
+ end
12
+ end
@@ -0,0 +1,34 @@
1
+ class Rethinker::QueryRunner::WriteError < Rethinker::QueryRunner::Middleware
2
+ def call(env)
3
+ @runner.call(env).tap do |result|
4
+ # TODO Fix rethinkdb driver: Their classes Term, Query, Response are
5
+ # not scoped to the RethinkDB module! (that would prevent a user from
6
+ # creating a Response model for example).
7
+
8
+ if is_write_query?(env) && (result['errors'].to_i != 0 || result['skipped'].to_i != 0)
9
+ raise_write_error(env, result['first_error'])
10
+ end
11
+ end
12
+ rescue RethinkDB::RqlRuntimeError => e
13
+ raise unless is_write_query?(env)
14
+
15
+ error_msg = e.message.split("\nBacktrace").first
16
+ error_msg = "Non existent document" if e.message =~ /Expected type OBJECT but found NULL/
17
+ raise_write_error(env, error_msg)
18
+ end
19
+
20
+ private
21
+
22
+ def is_write_query?(env)
23
+ env[:query].body.type.in?([Term::TermType::UPDATE,
24
+ Term::TermType::DELETE,
25
+ Term::TermType::REPLACE,
26
+ Term::TermType::INSERT])
27
+ end
28
+
29
+ def raise_write_error(env, error_msg)
30
+ error_msg ||= "Unknown error"
31
+ error_msg += "\nQuery was: #{env[:query].inspect[0..1000]}"
32
+ raise Rethinker::Error::DocumentNotSaved, error_msg
33
+ end
34
+ end
@@ -0,0 +1,16 @@
1
+ require "rethinker"
2
+ require "rails"
3
+
4
+ class Rethinker::Railtie < Rails::Railtie
5
+ config.action_dispatch.rescue_responses.merge!(
6
+ "Rethinker::Errors::DocumentNotFound" => :not_found,
7
+ "Rethinker::Errors::DocumentInvalid" => :unprocessable_entity,
8
+ "Rethinker::Errors::DocumentNotSaved" => :unprocessable_entity,
9
+ )
10
+
11
+ rake_tasks do
12
+ load "rethinker/railtie/database.rake"
13
+ end
14
+
15
+ #config.eager_load_namespaces << Rethinker
16
+ end
@@ -0,0 +1,37 @@
1
+ namespace :db do
2
+ unless Rake::Task.task_defined?("db:drop")
3
+ desc 'Truncate all the tables'
4
+ task :drop => :environment do
5
+ Rethinker.purge!
6
+ end
7
+ end
8
+
9
+ unless Rake::Task.task_defined?("db:seed")
10
+ desc 'Load the seed data from db/seeds.rb'
11
+ task :seed => :environment do
12
+ Rails.application.load_seed
13
+ end
14
+ end
15
+
16
+ unless Rake::Task.task_defined?("db:setup")
17
+ desc 'Equivalent to db:seed'
18
+ task :setup => [ 'db:seed' ]
19
+ end
20
+
21
+ unless Rake::Task.task_defined?("db:reset")
22
+ desc 'Equivalent to db:drop + db:seed'
23
+ task :reset => [ 'db:drop', 'db:seed' ]
24
+ end
25
+
26
+ unless Rake::Task.task_defined?("db:create")
27
+ task :create => :environment do
28
+ # noop
29
+ end
30
+ end
31
+
32
+ unless Rake::Task.task_defined?("db:migrate")
33
+ task :migrate => :environment do
34
+ # noop
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,6 @@
1
+ module Rethinker::Relation
2
+ extend Rethinker::Autoload
3
+
4
+ autoload :BelongsTo, :HasMany
5
+ # you also want to check Rethinker::Document::Relation
6
+ end
@@ -0,0 +1,36 @@
1
+ class Rethinker::Relation::BelongsTo < Struct.new(:children_klass, :parent_name, :options)
2
+ def foreign_key
3
+ # TODO test :foreign_key
4
+ @foreign_key ||= options[:foreign_key] || :"#{parent_name}_id"
5
+ end
6
+
7
+ def parent_klass_lazy
8
+ # TODO test :class_name
9
+ @parent_klass_lazy ||= options[:class_name] || parent_name.to_s.camelize
10
+ end
11
+
12
+ def hook
13
+ # TODO yell when some options are not recognized
14
+ children_klass.field foreign_key
15
+
16
+ children_klass.inject_in_layer :relations, <<-RUBY, __FILE__, __LINE__ + 1
17
+ def #{foreign_key}=(value)
18
+ super
19
+ @relations_cache[:#{parent_name}] = nil
20
+ end
21
+
22
+ def #{parent_name}=(new_parent)
23
+ # TODO raise when new_parent doesn't have the proper type
24
+ new_parent.save! if new_parent && !new_parent.persisted?
25
+ self.#{foreign_key} = new_parent.try(:id)
26
+ @relations_cache[:#{parent_name}] = new_parent
27
+ end
28
+
29
+ def #{parent_name}
30
+ if #{foreign_key}
31
+ @relations_cache[:#{parent_name}] ||= #{parent_klass_lazy}.find(#{foreign_key})
32
+ end
33
+ end
34
+ RUBY
35
+ end
36
+ end
@@ -0,0 +1,30 @@
1
+ class Rethinker::Relation::HasMany < Struct.new(:parent_klass, :children_name, :options)
2
+ extend ActiveSupport::Autoload
3
+ autoload :Selection
4
+
5
+ def foreign_key
6
+ # TODO test :foreign_key
7
+ @foreign_key ||= options[:foreign_key] || :"#{parent_klass.name.underscore}_id"
8
+ end
9
+
10
+ def children_klass
11
+ # TODO test :class_name
12
+ @children_klass ||= (options[:class_name] || children_name.to_s.singularize.camelize).constantize
13
+ end
14
+
15
+ def hook
16
+ # TODO yell when some options are not recognized
17
+ parent_klass.inject_in_layer :relations, <<-RUBY, __FILE__, __LINE__ + 1
18
+ def #{children_name}=(new_children)
19
+ #{children_name}.destroy
20
+ new_children.each { |child| #{children_name} << child }
21
+ end
22
+
23
+ def #{children_name}
24
+ # TODO Cache array
25
+ relation = self.class.relations[:#{children_name}]
26
+ ::Rethinker::Relation::HasMany::Selection.new(self, relation)
27
+ end
28
+ RUBY
29
+ end
30
+ end
@@ -0,0 +1,27 @@
1
+ class Rethinker::Relation::HasMany::Selection < Rethinker::Selection
2
+ attr_accessor :parent_instance, :relation
3
+ delegate :foreign_key, :children_klass, :to => :relation
4
+
5
+ def initialize(parent_instance, relation)
6
+ self.relation = relation
7
+ self.parent_instance = parent_instance
8
+ super children_klass.where(foreign_key => parent_instance.id).criteria, klass: children_klass
9
+ end
10
+
11
+ def <<(child)
12
+ # TODO raise when child doesn't have the proper type
13
+ child.update_attributes(foreign_key => parent_instance.id)
14
+ end
15
+
16
+ def build(attrs={})
17
+ children_klass.new(attrs.merge(foreign_key => parent_instance.id))
18
+ end
19
+
20
+ def create(*args)
21
+ build(*args).tap { |doc| doc.save }
22
+ end
23
+
24
+ def create!(*args)
25
+ build(*args).tap { |doc| doc.save! }
26
+ end
27
+ end
@@ -0,0 +1,6 @@
1
+ class Rethinker::Selection
2
+ extend Rethinker::Autoload
3
+
4
+ autoload_and_include :Core, :Count, :Delete, :Enumerable, :First, :Inc,
5
+ :Limit, :OrderBy, :Scope, :Update, :Where
6
+ end
@@ -0,0 +1,39 @@
1
+ module Rethinker::Selection::Core
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ attr_accessor :context, :criteria
6
+ delegate :inspect, to: :query
7
+ end
8
+
9
+ def initialize(criteria = [], context = {})
10
+ # We are saving klass as a context
11
+ # so that the table_on_demand middleware can do its job
12
+ # TODO FIXME Sadly it gets funny with associations
13
+ self.context = context
14
+ self.criteria = [criteria].flatten
15
+ self
16
+ end
17
+
18
+ def klass
19
+ context[:klass]
20
+ end
21
+
22
+ def chain(criterion)
23
+ self.criteria.concat([criterion]).flatten! unless criterion.blank?
24
+ self
25
+ end
26
+
27
+ def run
28
+ Rethinker.run { self.query }
29
+ end
30
+
31
+ def query
32
+ chained_query = nil
33
+ self.criteria.each do |criterion|
34
+ chained_query = criterion.execute(chained_query, self.context)
35
+ end
36
+ chained_query
37
+ end
38
+
39
+ end
@@ -0,0 +1,13 @@
1
+ module Rethinker::Selection::Count
2
+ def count
3
+ chain(Rethinker::Criterion.new(:count)).run
4
+ end
5
+
6
+ def empty?
7
+ count == 0
8
+ end
9
+
10
+ def any?
11
+ !empty?
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ module Rethinker::Selection::Delete
2
+ def delete
3
+ chain(Rethinker::Criterion.new(:delete)).run
4
+ end
5
+
6
+ def destroy
7
+ each { |doc| doc.destroy }
8
+ end
9
+ end
@@ -0,0 +1,22 @@
1
+ module Rethinker::Selection::Enumerable
2
+ def each(&block)
3
+ return enum_for(:each) unless block
4
+
5
+ klass.ensure_table! # needed as soon as we get a Query_Result
6
+ run.each do |attrs|
7
+ yield klass.new_from_db(attrs)
8
+ end
9
+ self
10
+ end
11
+
12
+ # TODO test that
13
+ def respond_to?(name, include_private = false)
14
+ super || [].respond_to?(name)
15
+ end
16
+
17
+ # TODO Make something a bit more efficent ?
18
+ def method_missing(name, *args, &block)
19
+ return super unless [].respond_to?(name)
20
+ each.__send__(name, *args, &block)
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ module Rethinker::Selection::First
2
+ def first
3
+ self.context[:order] = :normal
4
+ get_one
5
+ end
6
+
7
+ def last
8
+ self.context[:order] = :reverse
9
+ get_one
10
+ end
11
+
12
+ private
13
+
14
+ def get_one
15
+ klass.ensure_table! # needed as soon as we get a Query_Result
16
+ order_by(:id) unless ordered?
17
+ attrs = chain(Rethinker::Criterion.new(:limit, 1)).run.first
18
+ klass.new_from_db(attrs)
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ module Rethinker::Selection::Inc
2
+ def inc(field, value=1)
3
+ # TODO The useful inc() is on a model instance.
4
+ # But then do we want to postpone the inc() to the next save?
5
+ # It might make sense (because we don't have transactions).
6
+ update { |doc| { field => doc[field] + value } }
7
+ end
8
+
9
+ def dec(field, value=1)
10
+ inc(field, -value)
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ module Rethinker::Selection::Limit
2
+ # TODO Test these guys
3
+
4
+ def limit(value)
5
+ chain Rethinker::Criterion.new(:limit, value)
6
+ end
7
+
8
+ def skip(value)
9
+ chain Rethinker::Criterion.new(:skip, value)
10
+ end
11
+
12
+ def [](sel)
13
+ chain Rethinker::Criterion.new(:[], sel)
14
+ end
15
+ end