rethinker 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.md +10 -0
- data/README.md +49 -0
- data/USAGE.rb.md +99 -0
- data/lib/rethinker.rb +38 -0
- data/lib/rethinker/autoload.rb +14 -0
- data/lib/rethinker/connection.rb +49 -0
- data/lib/rethinker/criterion.rb +32 -0
- data/lib/rethinker/database.rb +38 -0
- data/lib/rethinker/document.rb +8 -0
- data/lib/rethinker/document/attributes.rb +117 -0
- data/lib/rethinker/document/core.rb +37 -0
- data/lib/rethinker/document/dynamic_attributes.rb +12 -0
- data/lib/rethinker/document/id.rb +54 -0
- data/lib/rethinker/document/injection_layer.rb +11 -0
- data/lib/rethinker/document/persistence.rb +79 -0
- data/lib/rethinker/document/polymorphic.rb +37 -0
- data/lib/rethinker/document/relation.rb +34 -0
- data/lib/rethinker/document/selection.rb +49 -0
- data/lib/rethinker/document/serialization.rb +41 -0
- data/lib/rethinker/document/timestamps.rb +18 -0
- data/lib/rethinker/document/validation.rb +62 -0
- data/lib/rethinker/error.rb +6 -0
- data/lib/rethinker/query_runner.rb +32 -0
- data/lib/rethinker/query_runner/connection.rb +17 -0
- data/lib/rethinker/query_runner/database_on_demand.rb +16 -0
- data/lib/rethinker/query_runner/driver.rb +10 -0
- data/lib/rethinker/query_runner/selection.rb +8 -0
- data/lib/rethinker/query_runner/table_on_demand.rb +12 -0
- data/lib/rethinker/query_runner/write_error.rb +34 -0
- data/lib/rethinker/railtie.rb +16 -0
- data/lib/rethinker/railtie/database.rake +37 -0
- data/lib/rethinker/relation.rb +6 -0
- data/lib/rethinker/relation/belongs_to.rb +36 -0
- data/lib/rethinker/relation/has_many.rb +30 -0
- data/lib/rethinker/relation/has_many/selection.rb +27 -0
- data/lib/rethinker/selection.rb +6 -0
- data/lib/rethinker/selection/core.rb +39 -0
- data/lib/rethinker/selection/count.rb +13 -0
- data/lib/rethinker/selection/delete.rb +9 -0
- data/lib/rethinker/selection/enumerable.rb +22 -0
- data/lib/rethinker/selection/first.rb +20 -0
- data/lib/rethinker/selection/inc.rb +12 -0
- data/lib/rethinker/selection/limit.rb +15 -0
- data/lib/rethinker/selection/order_by.rb +41 -0
- data/lib/rethinker/selection/scope.rb +11 -0
- data/lib/rethinker/selection/update.rb +6 -0
- data/lib/rethinker/selection/where.rb +23 -0
- data/lib/rethinker/version.rb +3 -0
- metadata +140 -0
@@ -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,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,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,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,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
|