nobrainer 0.8.0

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 (61) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +7 -0
  3. data/README.md +6 -0
  4. data/lib/no_brainer/autoload.rb +14 -0
  5. data/lib/no_brainer/config.rb +61 -0
  6. data/lib/no_brainer/connection.rb +53 -0
  7. data/lib/no_brainer/criteria.rb +20 -0
  8. data/lib/no_brainer/criteria/chainable/core.rb +67 -0
  9. data/lib/no_brainer/criteria/chainable/limit.rb +31 -0
  10. data/lib/no_brainer/criteria/chainable/order_by.rb +76 -0
  11. data/lib/no_brainer/criteria/chainable/raw.rb +25 -0
  12. data/lib/no_brainer/criteria/chainable/scope.rb +41 -0
  13. data/lib/no_brainer/criteria/chainable/where.rb +198 -0
  14. data/lib/no_brainer/criteria/termination/cache.rb +71 -0
  15. data/lib/no_brainer/criteria/termination/count.rb +19 -0
  16. data/lib/no_brainer/criteria/termination/delete.rb +11 -0
  17. data/lib/no_brainer/criteria/termination/eager_loading.rb +64 -0
  18. data/lib/no_brainer/criteria/termination/enumerable.rb +24 -0
  19. data/lib/no_brainer/criteria/termination/first.rb +25 -0
  20. data/lib/no_brainer/criteria/termination/inc.rb +14 -0
  21. data/lib/no_brainer/criteria/termination/update.rb +13 -0
  22. data/lib/no_brainer/database.rb +41 -0
  23. data/lib/no_brainer/decorated_symbol.rb +15 -0
  24. data/lib/no_brainer/document.rb +18 -0
  25. data/lib/no_brainer/document/association.rb +41 -0
  26. data/lib/no_brainer/document/association/belongs_to.rb +64 -0
  27. data/lib/no_brainer/document/association/core.rb +64 -0
  28. data/lib/no_brainer/document/association/has_many.rb +68 -0
  29. data/lib/no_brainer/document/attributes.rb +124 -0
  30. data/lib/no_brainer/document/core.rb +20 -0
  31. data/lib/no_brainer/document/criteria.rb +62 -0
  32. data/lib/no_brainer/document/dirty.rb +88 -0
  33. data/lib/no_brainer/document/dynamic_attributes.rb +12 -0
  34. data/lib/no_brainer/document/id.rb +49 -0
  35. data/lib/no_brainer/document/index.rb +102 -0
  36. data/lib/no_brainer/document/injection_layer.rb +12 -0
  37. data/lib/no_brainer/document/persistance.rb +124 -0
  38. data/lib/no_brainer/document/polymorphic.rb +43 -0
  39. data/lib/no_brainer/document/serialization.rb +9 -0
  40. data/lib/no_brainer/document/store_in.rb +33 -0
  41. data/lib/no_brainer/document/timestamps.rb +18 -0
  42. data/lib/no_brainer/document/validation.rb +35 -0
  43. data/lib/no_brainer/error.rb +10 -0
  44. data/lib/no_brainer/fork.rb +14 -0
  45. data/lib/no_brainer/index_manager.rb +6 -0
  46. data/lib/no_brainer/loader.rb +5 -0
  47. data/lib/no_brainer/locale/en.yml +4 -0
  48. data/lib/no_brainer/query_runner.rb +37 -0
  49. data/lib/no_brainer/query_runner/connection.rb +17 -0
  50. data/lib/no_brainer/query_runner/database_on_demand.rb +26 -0
  51. data/lib/no_brainer/query_runner/driver.rb +8 -0
  52. data/lib/no_brainer/query_runner/logger.rb +29 -0
  53. data/lib/no_brainer/query_runner/run_options.rb +34 -0
  54. data/lib/no_brainer/query_runner/table_on_demand.rb +44 -0
  55. data/lib/no_brainer/query_runner/write_error.rb +28 -0
  56. data/lib/no_brainer/railtie.rb +36 -0
  57. data/lib/no_brainer/railtie/database.rake +34 -0
  58. data/lib/no_brainer/util.rb +23 -0
  59. data/lib/no_brainer/version.rb +3 -0
  60. data/lib/nobrainer.rb +59 -0
  61. metadata +152 -0
@@ -0,0 +1,68 @@
1
+ class NoBrainer::Document::Association::HasMany
2
+ include NoBrainer::Document::Association::Core
3
+
4
+ class Metadata
5
+ VALID_HAS_MANY_OPTIONS = [:foreign_key, :class_name, :dependent]
6
+ include NoBrainer::Document::Association::Core::Metadata
7
+
8
+ def foreign_key
9
+ # TODO test :foreign_key
10
+ options[:foreign_key].try(:to_sym) || :"#{owner_klass.name.underscore}_id"
11
+ end
12
+
13
+ def target_klass
14
+ # TODO test :class_name
15
+ (options[:class_name] || target_name.to_s.singularize.camelize).constantize
16
+ end
17
+
18
+ def hook
19
+ super
20
+ options.assert_valid_keys(*VALID_HAS_MANY_OPTIONS)
21
+ add_callback_for(:before_destroy)
22
+ end
23
+
24
+ def eager_load(docs, criteria=nil)
25
+ target_criteria = target_klass.all
26
+ target_criteria = target_criteria.merge(criteria) if criteria
27
+ docs_ids = Hash[docs.map { |doc| [doc, doc.id] }]
28
+ fk_targets = target_criteria
29
+ .where(foreign_key.in => docs_ids.values)
30
+ .reduce({}) do |hash, doc|
31
+ fk = doc.read_attribute(foreign_key)
32
+ hash[fk] ||= []
33
+ hash[fk] << doc
34
+ hash
35
+ end
36
+ docs_ids.each { |doc, id| doc.association(self)._write(fk_targets[id]) if fk_targets[id] }
37
+ fk_targets.values.flatten(1)
38
+ end
39
+ end
40
+
41
+ def children_criteria
42
+ @children_criteria ||= target_klass.where(foreign_key => instance.id)
43
+ end
44
+
45
+ def read
46
+ children_criteria
47
+ end
48
+
49
+ def write(new_children)
50
+ raise "You can't assign the array of #{target_name}. Instead, you must modify delete and create #{target_klass} manually."
51
+ end
52
+
53
+ def _write(new_children)
54
+ children_criteria._override_cache(new_children)
55
+ end
56
+
57
+ def before_destroy_callback
58
+ criteria = children_criteria.unscoped
59
+ case metadata.options[:dependent]
60
+ when nil then
61
+ when :destroy then criteria.destroy_all
62
+ when :delete then criteria.delete_all
63
+ when :nullify then criteria.update_all(foreign_key => nil)
64
+ when :restrict then raise NoBrainer::Error::ChildrenExist unless criteria.count.zero?
65
+ else raise "Unrecognized dependent option: #{metadata.options[:dependent]}"
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,124 @@
1
+ module NoBrainer::Document::Attributes
2
+ VALID_FIELD_OPTIONS = [:index, :default]
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ # Not using class_attribute because we want to
7
+ # use our custom logic
8
+ class << self; attr_accessor :fields; end
9
+ self.fields = {}
10
+ end
11
+
12
+ def initialize(attrs={}, options={})
13
+ super
14
+ @attributes = {}
15
+ assign_attributes(attrs, options.reverse_merge(:pristine => true))
16
+ end
17
+
18
+ def attributes
19
+ @attributes.dup.freeze
20
+ end
21
+
22
+ def read_attribute(name)
23
+ __send__("#{name}")
24
+ end
25
+ def [](*args); read_attribute(*args); end
26
+
27
+ def write_attribute(name, value)
28
+ __send__("#{name}=", value)
29
+ end
30
+ def []=(*args); write_attribute(*args); end
31
+
32
+ def assign_defaults
33
+ self.class.fields.each do |name, field_options|
34
+ if field_options.has_key?(:default) && !@attributes.has_key?(name.to_s)
35
+ default_value = field_options[:default]
36
+ default_value = default_value.call if default_value.is_a?(Proc)
37
+ self.write_attribute(name, default_value)
38
+ end
39
+ end
40
+ end
41
+
42
+ def assign_attributes(attrs, options={})
43
+ # XXX We don't save field that are not explicitly set. The row will
44
+ # therefore not contain nil for unset attributes.
45
+ @attributes.clear if options[:pristine]
46
+
47
+ if options[:from_db]
48
+ # TODO Should we reject undeclared fields ?
49
+ #
50
+ # TODO Not using the getter/setters, the dirty tracking won't notice it,
51
+ # also we should start thinking about custom serializer/deserializer.
52
+ @attributes.merge! attrs
53
+ else
54
+ attrs.each { |k,v| self.write_attribute(k, v) }
55
+ end
56
+
57
+ assign_defaults if options[:pristine] || options[:from_db]
58
+ self
59
+ end
60
+ def attributes=(*args); assign_attributes(*args); end
61
+
62
+ # TODO test that thing
63
+ def inspect
64
+ attrs = self.class.fields.keys.map { |f| "#{f}: #{@attributes[f.to_s].inspect}" }
65
+ "#<#{self.class} #{attrs.join(', ')}>"
66
+ end
67
+
68
+ def to_s
69
+ inspect
70
+ end
71
+
72
+ module ClassMethods
73
+ def new_from_db(attrs, options={})
74
+ klass_from_attrs(attrs).new(attrs, options.reverse_merge(:from_db => true)) if attrs
75
+ end
76
+
77
+ def inherited(subclass)
78
+ super
79
+ subclass.fields = self.fields.dup
80
+ end
81
+
82
+ def field(name, options={})
83
+ name = name.to_sym
84
+
85
+ options.assert_valid_keys(*NoBrainer::Document::Attributes::VALID_FIELD_OPTIONS)
86
+ if name.in? NoBrainer::Criteria::Chainable::Where::RESERVED_FIELDS
87
+ raise "Cannot use a reserved field name: #{name}"
88
+ end
89
+
90
+ ([self] + descendants).each do |klass|
91
+ klass.fields[name] ||= {}
92
+ klass.fields[name].merge!(options)
93
+ end
94
+
95
+ # Using a layer so the user can use super when overriding these methods
96
+ inject_in_layer :attributes, <<-RUBY, __FILE__, __LINE__ + 1
97
+ def #{name}=(value)
98
+ @attributes['#{name}'] = value
99
+ end
100
+
101
+ def #{name}
102
+ @attributes['#{name}']
103
+ end
104
+ RUBY
105
+ end
106
+
107
+ def remove_field(name)
108
+ name = name.to_sym
109
+
110
+ ([self] + descendants).each do |klass|
111
+ klass.fields.delete(name)
112
+ end
113
+
114
+ inject_in_layer :attributes, <<-RUBY, __FILE__, __LINE__ + 1
115
+ undef #{name}=
116
+ undef #{name}
117
+ RUBY
118
+ end
119
+
120
+ def has_field?(name)
121
+ !!fields[name.to_sym]
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,20 @@
1
+ module NoBrainer::Document::Core
2
+ extend ActiveSupport::Concern
3
+
4
+ class << self; attr_accessor :all; end
5
+ self.all = []
6
+
7
+ # TODO This assume the primary key is id.
8
+ # RethinkDB can have a custom primary key. careful.
9
+ include ActiveModel::Conversion
10
+
11
+ included do
12
+ # TODO test these includes
13
+ extend ActiveModel::Naming
14
+ extend ActiveModel::Translation
15
+
16
+ NoBrainer::Document::Core.all << self
17
+ end
18
+
19
+ def initialize(attrs={}, options={}); end
20
+ end
@@ -0,0 +1,62 @@
1
+ module NoBrainer::Document::Criteria
2
+ extend ActiveSupport::Concern
3
+
4
+ def selector
5
+ self.class.selector_for(id)
6
+ end
7
+
8
+ included do
9
+ class_attribute :default_scope_proc
10
+ end
11
+
12
+ module ClassMethods
13
+ delegate :to_rql, # Core
14
+ :limit, :offset, :skip, # Limit
15
+ :order_by, :reverse_order, # OrderBy
16
+ :scoped, :unscoped, # Scope
17
+ :where, :with_index, :without_index, :used_index, :indexed?, # Where
18
+ :with_cache, :without_cache, # Cache
19
+ :count, :empty?, :any?, # Count
20
+ :delete_all, :destroy_all, # Delete
21
+ :includes, # EagerLoading
22
+ :each, :to_a, # Enumerable
23
+ :first, :last, :first!, :last!, # First
24
+ :inc_all, :dec_all, # Inc
25
+ :update_all, :replace_all, # Update
26
+ :to => :all
27
+
28
+ def all
29
+ NoBrainer::Criteria.new(:klass => self)
30
+ end
31
+
32
+ def scope(name, criteria=nil, &block)
33
+ criteria ||= block
34
+ criteria_proc = criteria.is_a?(Proc) ? criteria : proc { criteria }
35
+ singleton_class.class_eval do
36
+ define_method(name) { |*args| criteria_proc.call(*args) }
37
+ end
38
+ end
39
+
40
+ def default_scope(criteria=nil, &block)
41
+ criteria ||= block
42
+ self.default_scope_proc = criteria.is_a?(Proc) ? criteria : proc { criteria }
43
+ end
44
+
45
+ def selector_for(id)
46
+ # TODO Pass primary key if not default
47
+ unscoped.where(:id => id)
48
+ end
49
+
50
+ # XXX this doesn't have the same semantics as
51
+ # other ORMs. the equivalent is find!.
52
+ def find(id)
53
+ selector_for(id).first
54
+ end
55
+
56
+ def find!(id)
57
+ find(id).tap do |doc|
58
+ raise NoBrainer::Error::DocumentNotFound, "#{self.class} id #{id} not found" unless doc
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,88 @@
1
+ module NoBrainer::Document::Dirty
2
+ extend ActiveSupport::Concern
3
+
4
+ # We are not using ActiveModel::Dirty because it's using
5
+ # ActiveModel::AttributeMethods which gives pretty violent method_missing()
6
+ # capabilities, such as giving a getter/setter method for any keys within the
7
+ # attributes keys. We don't want that.
8
+
9
+ included do
10
+ attr_accessor :previous_changes
11
+ after_save { clear_dirtiness }
12
+ end
13
+
14
+ def changed_attributes
15
+ @changed_attributes ||= {}
16
+ end
17
+
18
+ def assign_attributes(attrs, options={})
19
+ clear_dirtiness if options[:pristine] || options[:from_db]
20
+ super
21
+ end
22
+
23
+ def clear_dirtiness
24
+ self.previous_changes = changes
25
+ self.changed_attributes.clear
26
+ end
27
+
28
+ def changed?
29
+ changed_attributes.present?
30
+ end
31
+
32
+ def changed
33
+ changed_attributes.keys
34
+ end
35
+
36
+ def changes
37
+ Hash[changed_attributes.map { |k,v| [k, [v, read_attribute(k)]] }]
38
+ end
39
+
40
+ def attribute_will_change!(attr, new_value)
41
+ return if changed_attributes.include?(attr)
42
+
43
+ # ActiveModel ignores TypeError and NoMethodError exception as if nothng
44
+ # happened. Why is that?
45
+ value = read_attribute(attr)
46
+ value = value.clone if value.duplicable?
47
+
48
+ return if value == new_value
49
+
50
+ changed_attributes[attr] = value
51
+ end
52
+
53
+ module ClassMethods
54
+ def field(name, options={})
55
+ super
56
+
57
+ inject_in_layer :dirty_tracking, <<-RUBY, __FILE__, __LINE__ + 1
58
+ def #{name}_changed?
59
+ changed_attributes.include?(:#{name})
60
+ end
61
+
62
+ def #{name}_change
63
+ [changed_attributes[:#{name}], #{name}] if #{name}_changed?
64
+ end
65
+
66
+ def #{name}_was
67
+ #{name}_changed? ? changed_attributes[:#{name}] : #{name}
68
+ end
69
+
70
+ def #{name}=(value)
71
+ attribute_will_change!(:#{name}, value)
72
+ super
73
+ end
74
+ RUBY
75
+ end
76
+
77
+ def remove_field(name)
78
+ super
79
+
80
+ inject_in_layer :dirty_tracking, <<-RUBY, __FILE__, __LINE__ + 1
81
+ undef #{name}_changed?
82
+ undef #{name}_change
83
+ undef #{name}_was
84
+ undef #{name}=
85
+ RUBY
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,12 @@
1
+ module NoBrainer::Document::DynamicAttributes
2
+ extend ActiveSupport::Concern
3
+
4
+ def read_attribute(name)
5
+ self.respond_to?("#{name}") ? super : @attributes[name.to_s]
6
+ end
7
+
8
+ def write_attribute(name, value)
9
+ attribute_will_change!(name, value)
10
+ self.respond_to?("#{name}=") ? super : @attributes[name.to_s] = value
11
+ end
12
+ end
@@ -0,0 +1,49 @@
1
+ require 'thread'
2
+ require 'socket'
3
+ require 'digest/md5'
4
+
5
+ module NoBrainer::Document::Id
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ self.field :id, :default => ->{ NoBrainer::Document::Id.generate }
10
+ end
11
+
12
+ def ==(other)
13
+ return super unless self.class == other.class
14
+ !id.nil? && id == other.id
15
+ end
16
+ alias_method :eql?, :==
17
+
18
+ delegate :hash, :to => :id
19
+
20
+ # The following code is inspired by the mongo-ruby-driver
21
+
22
+ @machine_id = Digest::MD5.digest(Socket.gethostname)[0, 3]
23
+ @lock = Mutex.new
24
+ @index = 0
25
+
26
+ def self.get_inc
27
+ @lock.synchronize do
28
+ @index = (@index + 1) % 0xFFFFFF
29
+ end
30
+ end
31
+
32
+ # TODO Unit test that thing
33
+ def self.generate
34
+ oid = ''
35
+ # 4 bytes current time
36
+ oid += [Time.now.to_i].pack("N")
37
+
38
+ # 3 bytes machine
39
+ oid += @machine_id
40
+
41
+ # 2 bytes pid
42
+ oid += [Process.pid % 0xFFFF].pack("n")
43
+
44
+ # 3 bytes inc
45
+ oid += [get_inc].pack("N")[1, 3]
46
+
47
+ oid.unpack("C12").map {|e| v=e.to_s(16); v.size == 1 ? "0#{v}" : v }.join
48
+ end
49
+ end
@@ -0,0 +1,102 @@
1
+ module NoBrainer::Document::Index
2
+ VALID_INDEX_OPTIONS = [:multi]
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :indexes
7
+ self.indexes = {}
8
+ self.index :id
9
+ end
10
+
11
+ module ClassMethods
12
+ def index(name, *args)
13
+ name = name.to_sym
14
+ options = args.extract_options!
15
+ options.assert_valid_keys(*NoBrainer::Document::Index::VALID_INDEX_OPTIONS)
16
+
17
+ raise "Too many arguments: #{args}" if args.size > 1
18
+
19
+ kind, what = case args.first
20
+ when nil then [:single, name.to_sym]
21
+ when Array then [:compound, args.first.map(&:to_sym)]
22
+ when Proc then [:proc, args.first]
23
+ else raise "Index argument must be a lambda or a list of fields"
24
+ end
25
+
26
+ # FIXME Primary key may not always be :id
27
+ if name.in?(NoBrainer::Criteria::Chainable::Where::RESERVED_FIELDS)
28
+ raise "Cannot use a reserved field name: #{name}"
29
+ end
30
+ if has_field?(name) && kind != :single
31
+ raise "Cannot reuse field name #{name}"
32
+ end
33
+
34
+ indexes[name] = {:kind => kind, :what => what, :options => options}
35
+ end
36
+
37
+ def remove_index(name)
38
+ indexes.delete(name.to_sym)
39
+ end
40
+
41
+ def has_index?(name)
42
+ !!indexes[name.to_sym]
43
+ end
44
+
45
+ def field(name, options={})
46
+ name = name.to_sym
47
+
48
+ if has_index?(name) && indexes[name][:kind] != :single
49
+ raise "Cannot reuse index name #{name}"
50
+ end
51
+
52
+ super
53
+
54
+ case options[:index]
55
+ when nil then
56
+ when Hash then index(name, options[:index])
57
+ when Symbol then index(name, options[:index] => true)
58
+ when true then index(name)
59
+ when false then remove_index(name)
60
+ end
61
+ end
62
+
63
+ def remove_field(name)
64
+ remove_index(name) if fields[name.to_sym][:index]
65
+ super
66
+ end
67
+
68
+ def perform_create_index(index_name, options={})
69
+ index_name = index_name.to_sym
70
+ index_args = self.indexes[index_name]
71
+
72
+ index_proc = case index_args[:kind]
73
+ when :single then nil
74
+ when :compound then ->(doc) { index_args[:what].map { |field| doc[field] } }
75
+ when :proc then index_args[:what]
76
+ end
77
+
78
+ NoBrainer.run(self.rql_table.index_create(index_name, index_args[:options], &index_proc))
79
+ NoBrainer.run(self.rql_table.index_wait(index_name)) if options[:wait]
80
+ STDERR.puts "Created index #{self}.#{index_name}" if options[:verbose]
81
+ end
82
+
83
+ def perform_drop_index(index_name, options={})
84
+ NoBrainer.run(self.rql_table.index_drop(index_name))
85
+ STDERR.puts "Dropped index #{self}.#{index_name}" if options[:verbose]
86
+ end
87
+
88
+ def perform_update_indexes(options={})
89
+ current_indexes = NoBrainer.run(self.rql_table.index_list).map(&:to_sym)
90
+ wanted_indexes = self.indexes.keys - [:id] # XXX Primary key?
91
+
92
+ (current_indexes - wanted_indexes).each do |index_name|
93
+ perform_drop_index(index_name, options)
94
+ end
95
+
96
+ (wanted_indexes - current_indexes).each do |index_name|
97
+ perform_create_index(index_name, options)
98
+ end
99
+ end
100
+ alias_method :update_indexes, :perform_update_indexes
101
+ end
102
+ end