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,37 @@
|
|
1
|
+
module Rethinker::Document::Core
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
# TODO This assume the primary key is id.
|
5
|
+
# RethinkDB can have a custom primary key. careful.
|
6
|
+
include ActiveModel::Conversion
|
7
|
+
|
8
|
+
included do
|
9
|
+
# TODO test these includes
|
10
|
+
extend ActiveModel::Naming
|
11
|
+
extend ActiveModel::Translation
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(attrs={}, options={}); end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def table_name
|
18
|
+
root_class.name.underscore.gsub('/', '__').pluralize
|
19
|
+
end
|
20
|
+
|
21
|
+
# Even though we are using class variables, it's threads-safe.
|
22
|
+
# It's still racy, but the race is harmless.
|
23
|
+
def table
|
24
|
+
root_class.class_eval do
|
25
|
+
@table ||= RethinkDB::RQL.new.table(table_name).freeze
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Thread safe because the operation is idempotent
|
30
|
+
# (no error if we try to create the table twice)
|
31
|
+
def ensure_table!
|
32
|
+
root_class.class_eval do
|
33
|
+
@table_created ||= !!self.count
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'socket'
|
3
|
+
require 'digest/md5'
|
4
|
+
|
5
|
+
module Rethinker::Document::Id
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
self.field :id
|
10
|
+
end
|
11
|
+
|
12
|
+
def reset_attributes
|
13
|
+
super
|
14
|
+
self.id = Rethinker::Document::Id.generate
|
15
|
+
end
|
16
|
+
|
17
|
+
def ==(other)
|
18
|
+
return super unless self.class == other.class
|
19
|
+
!id.nil? && id == other.id
|
20
|
+
end
|
21
|
+
alias_method :eql?, :==
|
22
|
+
|
23
|
+
delegate :hash, :to => :id
|
24
|
+
|
25
|
+
# The following code is inspired by the mongo-ruby-driver
|
26
|
+
|
27
|
+
@machine_id = Digest::MD5.digest(Socket.gethostname)[0, 3]
|
28
|
+
@lock = Mutex.new
|
29
|
+
@index = 0
|
30
|
+
|
31
|
+
def self.get_inc
|
32
|
+
@lock.synchronize do
|
33
|
+
@index = (@index + 1) % 0xFFFFFF
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# TODO Unit test that thing
|
38
|
+
def self.generate
|
39
|
+
oid = ''
|
40
|
+
# 4 bytes current time
|
41
|
+
oid += [Time.now.to_i].pack("N")
|
42
|
+
|
43
|
+
# 3 bytes machine
|
44
|
+
oid += @machine_id
|
45
|
+
|
46
|
+
# 2 bytes pid
|
47
|
+
oid += [Process.pid % 0xFFFF].pack("n")
|
48
|
+
|
49
|
+
# 3 bytes inc
|
50
|
+
oid += [get_inc].pack("N")[1, 3]
|
51
|
+
|
52
|
+
oid.unpack("C12").map {|e| v=e.to_s(16); v.size == 1 ? "0#{v}" : v }.join
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Rethinker::Document::InjectionLayer
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
module ClassMethods
|
5
|
+
def inject_in_layer(name, code, file, line)
|
6
|
+
class_eval <<-RUBY, file, line
|
7
|
+
include module RethinkerLayer; module #{name.to_s.camelize}; #{code}; self; end; end
|
8
|
+
RUBY
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Rethinker::Document::Persistence
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
extend ActiveModel::Callbacks
|
6
|
+
define_model_callbacks :create, :update, :save, :destroy
|
7
|
+
end
|
8
|
+
|
9
|
+
# TODO after_initialize, after_find callback
|
10
|
+
def initialize(attrs={}, options={})
|
11
|
+
super
|
12
|
+
@new_record = !options[:from_db]
|
13
|
+
end
|
14
|
+
|
15
|
+
def new_record?
|
16
|
+
@new_record
|
17
|
+
end
|
18
|
+
|
19
|
+
def destroyed?
|
20
|
+
!!@destroyed
|
21
|
+
end
|
22
|
+
|
23
|
+
def persisted?
|
24
|
+
!new_record? && !destroyed?
|
25
|
+
end
|
26
|
+
|
27
|
+
def _create
|
28
|
+
run_callbacks :create do
|
29
|
+
result = Rethinker.run { self.class.table.insert(attributes) }
|
30
|
+
self.id ||= result['generated_keys'].first
|
31
|
+
@new_record = false
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def reload
|
37
|
+
assign_attributes(selector.run, :pristine => true)
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def update(&block)
|
42
|
+
run_callbacks :update do
|
43
|
+
selector.update(&block)
|
44
|
+
true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def save(options={})
|
49
|
+
run_callbacks :save do
|
50
|
+
new_record? ? _create : update { attributes }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def update_attributes(attrs, options={})
|
55
|
+
assign_attributes(attrs, options)
|
56
|
+
save
|
57
|
+
end
|
58
|
+
|
59
|
+
def delete
|
60
|
+
selector.delete
|
61
|
+
@destroyed = true
|
62
|
+
# TODO freeze attributes
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
def destroy
|
67
|
+
run_callbacks(:destroy) { delete }
|
68
|
+
end
|
69
|
+
|
70
|
+
module ClassMethods
|
71
|
+
def create(*args)
|
72
|
+
new(*args).tap { |doc| doc.save }
|
73
|
+
end
|
74
|
+
|
75
|
+
def create!(*args)
|
76
|
+
new(*args).tap { |doc| doc.save! }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Rethinker::Document::Polymorphic
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
include ActiveSupport::DescendantsTracker
|
4
|
+
|
5
|
+
included do
|
6
|
+
class_attribute :root_class
|
7
|
+
self.root_class = self
|
8
|
+
end
|
9
|
+
|
10
|
+
def reset_attributes
|
11
|
+
super
|
12
|
+
self._type = self.class.type_value unless self.class.is_root_class?
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
def inherited(subclass)
|
17
|
+
super
|
18
|
+
subclass.field :_type if is_root_class?
|
19
|
+
end
|
20
|
+
|
21
|
+
def type_value
|
22
|
+
name
|
23
|
+
end
|
24
|
+
|
25
|
+
def descendants_type_values
|
26
|
+
([self] + descendants).map(&:type_value)
|
27
|
+
end
|
28
|
+
|
29
|
+
def is_root_class?
|
30
|
+
self == root_class
|
31
|
+
end
|
32
|
+
|
33
|
+
def klass_from_attrs(attrs)
|
34
|
+
attrs['_type'].try(:constantize) || root_class
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Rethinker::Document::Relation
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
class << self; attr_accessor :relations; end
|
6
|
+
self.relations = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def reset_attributes
|
10
|
+
super
|
11
|
+
@relations_cache = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def inherited(subclass)
|
16
|
+
super
|
17
|
+
subclass.relations = self.relations.dup
|
18
|
+
end
|
19
|
+
|
20
|
+
[:belongs_to, :has_many].each do |relation|
|
21
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
22
|
+
def #{relation}(target, options={})
|
23
|
+
target = target.to_sym
|
24
|
+
r = Rethinker::Relation::#{relation.to_s.camelize}.new(self, target, options)
|
25
|
+
r.hook
|
26
|
+
|
27
|
+
([self] + descendants).each do |klass|
|
28
|
+
klass.relations[target] = r
|
29
|
+
end
|
30
|
+
end
|
31
|
+
RUBY
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Rethinker::Document::Selection
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
def selector
|
5
|
+
self.class.selector_for(id)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def all
|
10
|
+
sel = Rethinker::Selection.new(Rethinker::Criterion.new(:table, table_name), :klass => self)
|
11
|
+
|
12
|
+
unless is_root_class?
|
13
|
+
# TODO use this: sel = sel.where(:_type.in(descendants_type_values))
|
14
|
+
sel = sel.where do |doc|
|
15
|
+
doc.has_fields(:_type) &
|
16
|
+
descendants_type_values.map { |type| doc[:_type].eq(type) }
|
17
|
+
.reduce { |a,b| a | b }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
sel
|
22
|
+
end
|
23
|
+
|
24
|
+
def scope(name, selection)
|
25
|
+
singleton_class.class_eval do
|
26
|
+
define_method(name) { |*args| selection.call(*args) }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
delegate :count, :where, :order_by, :first, :last, :to => :all
|
31
|
+
|
32
|
+
def selector_for(id)
|
33
|
+
# TODO Pass primary key if not default
|
34
|
+
Rethinker::Selection.new([Rethinker::Criterion.new(:table, table_name), Rethinker::Criterion.new(:get, id)], :klass => self)
|
35
|
+
end
|
36
|
+
|
37
|
+
# XXX this doesn't have the same semantics as
|
38
|
+
# other ORMs. the equivalent is find!.
|
39
|
+
def find(id)
|
40
|
+
new_from_db(selector_for(id).run)
|
41
|
+
end
|
42
|
+
|
43
|
+
def find!(id)
|
44
|
+
find(id).tap do |doc|
|
45
|
+
doc or raise Rethinker::Error::DocumentNotFound, "#{self.class} id #{id} not found"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Rethinker::Document::Serialization
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
include ActiveModel::Serialization
|
5
|
+
include ActiveModel::Serializers::JSON
|
6
|
+
include ActiveModel::Serializers::Xml
|
7
|
+
|
8
|
+
included { self.include_root_in_json = false }
|
9
|
+
|
10
|
+
# XXX This is a giant copy paste from lib/active_model/serialization.rb
|
11
|
+
# The diff is:
|
12
|
+
# - attribute_names = attributes.keys.sort
|
13
|
+
# + attribute_names = self.class.fields.keys.sort
|
14
|
+
# That's all.
|
15
|
+
def serializable_hash(options = nil)
|
16
|
+
options ||= {}
|
17
|
+
|
18
|
+
attribute_names = self.class.fields.keys.sort
|
19
|
+
if only = options[:only]
|
20
|
+
attribute_names &= Array.wrap(only).map(&:to_s)
|
21
|
+
elsif except = options[:except]
|
22
|
+
attribute_names -= Array.wrap(except).map(&:to_s)
|
23
|
+
end
|
24
|
+
|
25
|
+
hash = {}
|
26
|
+
attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
|
27
|
+
|
28
|
+
method_names = Array.wrap(options[:methods]).select { |n| respond_to?(n) }
|
29
|
+
method_names.each { |n| hash[n] = send(n) }
|
30
|
+
|
31
|
+
serializable_add_includes(options) do |association, records, opts|
|
32
|
+
hash[association] = if records.is_a?(Enumerable)
|
33
|
+
records.map { |a| a.serializable_hash(opts) }
|
34
|
+
else
|
35
|
+
records.serializable_hash(opts)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
hash
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Rethinker::Document::Timestamps
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
self.field :created_at
|
6
|
+
self.field :updated_at
|
7
|
+
|
8
|
+
before_create { self.created_at = Time.now if self.respond_to?(:created_at=) }
|
9
|
+
before_save { self.updated_at = Time.now if self.respond_to?(:updated_at=) }
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def disable_timestamps
|
14
|
+
self.remove_field :created_at
|
15
|
+
self.remove_field :updated_at
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Rethinker::Document::Validation
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
include ActiveModel::Validations
|
4
|
+
include ActiveModel::Validations::Callbacks
|
5
|
+
|
6
|
+
def save(options={})
|
7
|
+
options = options.reverse_merge(:validate => true)
|
8
|
+
|
9
|
+
if options[:validate]
|
10
|
+
valid? ? super : false
|
11
|
+
else
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# TODO Test that thing
|
17
|
+
def valid?(context=nil)
|
18
|
+
super(context || (new_record? ? :create : :update))
|
19
|
+
end
|
20
|
+
|
21
|
+
[:save, :update_attributes].each do |method|
|
22
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
23
|
+
def #{method}!(*args)
|
24
|
+
#{method}(*args) or raise Rethinker::Error::DocumentInvalid, errors
|
25
|
+
end
|
26
|
+
RUBY
|
27
|
+
end
|
28
|
+
|
29
|
+
class UniquenessValidator < ActiveModel::EachValidator
|
30
|
+
# Validate the document for uniqueness violations.
|
31
|
+
#
|
32
|
+
# @example Validate the document.
|
33
|
+
# validate_each(person, :title, "Sir")
|
34
|
+
#
|
35
|
+
# @param [ Document ] document The document to validate.
|
36
|
+
# @param [ Symbol ] attribute The field to validate on.
|
37
|
+
# @param [ Object ] value The value of the field.
|
38
|
+
#
|
39
|
+
# @return [ Boolean ] true if the attribute is unique.
|
40
|
+
def validate_each(document, attribute, value)
|
41
|
+
finder = document.class.where(attribute => value)
|
42
|
+
finder = apply_scopes(finder, document)
|
43
|
+
finder = exclude_document(finder, document) if document.persisted?
|
44
|
+
is_unique = finder.count == 0
|
45
|
+
unless is_unique
|
46
|
+
document.errors.add(attribute, 'is already taken')
|
47
|
+
end
|
48
|
+
is_unique
|
49
|
+
end
|
50
|
+
|
51
|
+
def apply_scopes(finder, document)
|
52
|
+
Array.wrap(options[:scope]).each do |scope_item|
|
53
|
+
finder = finder.where{|doc| doc[scope_item.to_s].eq(document.attributes[scope_item.to_s])}
|
54
|
+
end
|
55
|
+
finder
|
56
|
+
end
|
57
|
+
|
58
|
+
def exclude_document(finder, document)
|
59
|
+
finder.where{|doc| doc["id"].ne(document.attributes["id"])}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|