nobrainer 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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