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,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,12 @@
1
+ module Rethinker::Document::DynamicAttributes
2
+ extend ActiveSupport::Concern
3
+
4
+ def [](name)
5
+ attributes[name.to_s]
6
+ end
7
+
8
+ def []=(name, value)
9
+ attributes[name.to_s] = value
10
+ end
11
+
12
+ 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