rethinker 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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