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