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.
- 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
|