nobrainer 0.15.0 → 0.16.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/autoload.rb +2 -2
  3. data/lib/no_brainer/criteria.rb +3 -2
  4. data/lib/no_brainer/criteria/after_find.rb +1 -1
  5. data/lib/no_brainer/criteria/aggregate.rb +25 -0
  6. data/lib/no_brainer/criteria/core.rb +15 -1
  7. data/lib/no_brainer/criteria/count.rb +1 -1
  8. data/lib/no_brainer/criteria/delete.rb +2 -2
  9. data/lib/no_brainer/criteria/index.rb +37 -0
  10. data/lib/no_brainer/criteria/order_by.rb +53 -13
  11. data/lib/no_brainer/criteria/pluck.rb +81 -0
  12. data/lib/no_brainer/criteria/raw.rb +12 -4
  13. data/lib/no_brainer/criteria/scope.rb +11 -6
  14. data/lib/no_brainer/criteria/update.rb +12 -4
  15. data/lib/no_brainer/criteria/where.rb +133 -64
  16. data/lib/no_brainer/document.rb +4 -3
  17. data/lib/no_brainer/document/aliases.rb +55 -0
  18. data/lib/no_brainer/document/association.rb +1 -1
  19. data/lib/no_brainer/document/association/belongs_to.rb +2 -2
  20. data/lib/no_brainer/document/attributes.rb +26 -8
  21. data/lib/no_brainer/document/criteria.rb +7 -4
  22. data/lib/no_brainer/document/dirty.rb +21 -4
  23. data/lib/no_brainer/document/dynamic_attributes.rb +4 -1
  24. data/lib/no_brainer/document/index.rb +47 -12
  25. data/lib/no_brainer/document/lazy_fetch.rb +72 -0
  26. data/lib/no_brainer/document/missing_attributes.rb +73 -0
  27. data/lib/no_brainer/document/persistance.rb +57 -8
  28. data/lib/no_brainer/document/polymorphic.rb +14 -8
  29. data/lib/no_brainer/document/types.rb +5 -2
  30. data/lib/no_brainer/document/types/binary.rb +23 -0
  31. data/lib/no_brainer/document/uniqueness.rb +1 -1
  32. data/lib/no_brainer/error.rb +16 -0
  33. data/lib/no_brainer/index_manager.rb +1 -0
  34. data/lib/no_brainer/query_runner.rb +3 -1
  35. data/lib/no_brainer/query_runner/connection_lock.rb +7 -0
  36. data/lib/no_brainer/query_runner/database_on_demand.rb +6 -3
  37. data/lib/no_brainer/query_runner/logger.rb +22 -6
  38. data/lib/no_brainer/query_runner/missing_index.rb +5 -3
  39. data/lib/no_brainer/query_runner/table_on_demand.rb +6 -3
  40. data/lib/no_brainer/railtie.rb +1 -1
  41. metadata +12 -4
@@ -4,9 +4,10 @@ module NoBrainer::Document
4
4
  extend ActiveSupport::Concern
5
5
  extend NoBrainer::Autoload
6
6
 
7
- autoload_and_include :Core, :StoreIn, :InjectionLayer, :Attributes, :Readonly, :Validation, :Types,
8
- :Persistance, :Uniqueness, :Callbacks, :Dirty, :Id, :Association, :Serialization,
9
- :Criteria, :Polymorphic, :Index
7
+ autoload_and_include :Core, :StoreIn, :InjectionLayer, :Attributes, :Readonly,
8
+ :Validation, :Persistance, :Types, :Uniqueness, :Callbacks, :Dirty, :Id,
9
+ :Association, :Serialization, :Criteria, :Polymorphic, :Index, :Aliases,
10
+ :MissingAttributes, :LazyFetch
10
11
 
11
12
  autoload :DynamicAttributes, :Timestamps
12
13
 
@@ -0,0 +1,55 @@
1
+ module NoBrainer::Document::Aliases
2
+ extend ActiveSupport::Concern
3
+
4
+ # Index aliases are built-in index.rb
5
+
6
+ included do
7
+ # We ignore polymorphism for aliases.
8
+ cattr_accessor :alias_map, :alias_reverse_map, :instance_accessor => false
9
+ self.alias_map = {}
10
+ self.alias_reverse_map = {}
11
+ end
12
+
13
+ module ClassMethods
14
+ def _field(attr, options={})
15
+ if options[:as]
16
+ self.alias_map[attr.to_s] = options[:as].to_s
17
+ self.alias_reverse_map[options[:as].to_s] = attr.to_s
18
+ end
19
+ super
20
+ end
21
+
22
+ def _remove_field(attr, options={})
23
+ super
24
+
25
+ self.alias_map.delete(attr.to_s)
26
+ self.alias_reverse_map.delete(attr.to_s)
27
+ end
28
+
29
+ def reverse_lookup_field_alias(attr)
30
+ alias_reverse_map[attr.to_s] || attr
31
+ end
32
+
33
+ def lookup_field_alias(attr)
34
+ alias_map[attr.to_s] || attr
35
+ end
36
+
37
+ def with_fields_reverse_aliased(attrs)
38
+ case attrs
39
+ when Array then attrs.map { |k| reverse_lookup_field_alias(k) }
40
+ when Hash then Hash[attrs.map { |k,v| [reverse_lookup_field_alias(k), v] }]
41
+ end
42
+ end
43
+
44
+ def with_fields_aliased(attrs)
45
+ case attrs
46
+ when Array then attrs.map { |k| lookup_field_alias(k) }
47
+ when Hash then Hash[attrs.map { |k,v| [lookup_field_alias(k), v] }]
48
+ end
49
+ end
50
+
51
+ def persistable_key(k)
52
+ lookup_field_alias(super)
53
+ end
54
+ end
55
+ end
@@ -16,8 +16,8 @@ module NoBrainer::Document::Association
16
16
 
17
17
  module ClassMethods
18
18
  def inherited(subclass)
19
- super
20
19
  subclass.association_metadata = self.association_metadata.dup
20
+ super
21
21
  end
22
22
 
23
23
  METHODS.each do |association|
@@ -2,7 +2,7 @@ class NoBrainer::Document::Association::BelongsTo
2
2
  include NoBrainer::Document::Association::Core
3
3
 
4
4
  class Metadata
5
- VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :index, :validates, :required]
5
+ VALID_OPTIONS = [:primary_key, :foreign_key, :class_name, :foreign_key_as, :index, :validates, :required]
6
6
  include NoBrainer::Document::Association::Core::Metadata
7
7
  extend NoBrainer::Document::Association::EagerLoader::Generic
8
8
 
@@ -29,7 +29,7 @@ class NoBrainer::Document::Association::BelongsTo
29
29
  # This would have the effect of loading all the models because they
30
30
  # are likely to be related to each other. So we don't know the type
31
31
  # of the primary key of the target.
32
- owner_klass.field(foreign_key, :index => options[:index])
32
+ owner_klass.field(foreign_key, :as => options[:foreign_key_as], :index => options[:index])
33
33
  owner_klass.validates(target_name, { :presence => true }) if options[:required]
34
34
  owner_klass.validates(target_name, options[:validates]) if options[:validates]
35
35
 
@@ -1,5 +1,6 @@
1
1
  module NoBrainer::Document::Attributes
2
- VALID_FIELD_OPTIONS = [:index, :default, :type, :validates, :required, :unique, :in, :readonly, :primary_key]
2
+ VALID_FIELD_OPTIONS = [:index, :default, :type, :real_type, :validates, :required, :unique,
3
+ :in, :readonly, :primary_key, :as, :lazy_fetch]
3
4
  RESERVED_FIELD_NAMES = [:index, :default, :and, :or, :selector, :associations, :pk_value] \
4
5
  + NoBrainer::DecoratedSymbol::MODIFIERS.keys
5
6
  extend ActiveSupport::Concern
@@ -34,9 +35,18 @@ module NoBrainer::Document::Attributes
34
35
  end
35
36
  def []=(*args); write_attribute(*args); end
36
37
 
37
- def assign_defaults
38
+ def assign_defaults(options)
38
39
  self.class.fields.each do |name, field_options|
39
- if field_options.has_key?(:default) && !@_attributes.has_key?(name)
40
+ if field_options.has_key?(:default) &&
41
+ !@_attributes.has_key?(name)
42
+
43
+ if opt = options[:missing_attributes]
44
+ if (opt[:pluck] && !opt[:pluck][name]) ||
45
+ (opt[:without] && opt[:without][name])
46
+ next
47
+ end
48
+ end
49
+
40
50
  default_value = field_options[:default]
41
51
  default_value = default_value.call if default_value.is_a?(Proc)
42
52
  self.write_attribute(name, default_value)
@@ -45,15 +55,23 @@ module NoBrainer::Document::Attributes
45
55
  end
46
56
 
47
57
  def assign_attributes(attrs, options={})
48
- @_attributes.clear if options[:pristine]
58
+ if options[:pristine]
59
+ if options[:keep_ivars] && options[:missing_attributes].try(:[], :pluck)
60
+ options[:missing_attributes][:pluck].keys.each { |k| @_attributes.delete(k) }
61
+ else
62
+ @_attributes.clear
63
+ end
64
+ end
65
+
49
66
  if options[:from_db]
67
+ attrs = self.class.with_fields_reverse_aliased(attrs)
50
68
  @_attributes.merge!(attrs)
51
- clear_dirtiness
69
+ clear_dirtiness(options)
52
70
  else
53
- clear_dirtiness if options[:pristine]
71
+ clear_dirtiness(options) if options[:pristine]
54
72
  attrs.each { |k,v| self.write_attribute(k,v) }
55
73
  end
56
- assign_defaults if options[:pristine]
74
+ assign_defaults(options) if options[:pristine]
57
75
  self
58
76
  end
59
77
 
@@ -77,8 +95,8 @@ module NoBrainer::Document::Attributes
77
95
  end
78
96
 
79
97
  def inherited(subclass)
80
- super
81
98
  subclass.fields = self.fields.dup
99
+ super
82
100
  end
83
101
 
84
102
  def _field(attr, options={})
@@ -9,17 +9,21 @@ module NoBrainer::Document::Criteria
9
9
 
10
10
  module ClassMethods
11
11
  delegate :to_rql, # Core
12
+ :raw, # Raw
12
13
  :limit, :offset, :skip, # Limit
13
- :order_by, :reverse_order, :without_ordering, # OrderBy
14
+ :order_by, :reverse_order, :without_ordering, :order_by_indexed?, :order_by_index_name, # OrderBy
14
15
  :scoped, :unscoped, # Scope
15
- :where, :with_index, :without_index, :used_index, :indexed?, # Where
16
+ :where, :where_indexed?, :where_index_name, :where_index_type, # Where
17
+ :with_index, :without_index, :used_index, # Index
16
18
  :with_cache, :without_cache, # Cache
17
19
  :count, :empty?, :any?, # Count
18
20
  :delete_all, :destroy_all, # Delete
19
21
  :includes, :preload, # Preload
20
22
  :each, :to_a, # Enumerable
21
23
  :first, :last, :first!, :last!, :sample, # First
24
+ :min, :max, :sum, :avg, # Aggregate
22
25
  :update_all, :replace_all, # Update
26
+ :pluck, :without, :lazy_fetch, :without_plucking, # Pluck
23
27
  :to => :all
24
28
 
25
29
  def all
@@ -44,8 +48,7 @@ module NoBrainer::Document::Criteria
44
48
  rql_table.get(pk)
45
49
  end
46
50
 
47
- # XXX this doesn't have the same semantics as
48
- # other ORMs. the equivalent is find!.
51
+ # XXX this doesn't have the same semantics as other ORMs. the equivalent is find!.
49
52
  def find(pk)
50
53
  attrs = NoBrainer.run { selector_for(pk) }
51
54
  new_from_db(attrs).tap { |doc| doc.run_callbacks(:find) } if attrs
@@ -14,8 +14,14 @@ module NoBrainer::Document::Dirty
14
14
  super.tap { clear_dirtiness }
15
15
  end
16
16
 
17
- def clear_dirtiness
18
- @_old_attributes = {}.with_indifferent_access
17
+ def clear_dirtiness(options={})
18
+ if options[:keep_ivars] && options[:missing_attributes].try(:[], :pluck)
19
+ attrs = options[:missing_attributes][:pluck].keys
20
+ @_old_attributes = @_old_attributes.reject { |k,v| attrs.include?(k) }
21
+ else
22
+ @_old_attributes = {}.with_indifferent_access
23
+ end
24
+
19
25
  @_old_attributes_keys = @_attributes.keys # to track undefined -> nil changes
20
26
  end
21
27
 
@@ -38,7 +44,18 @@ module NoBrainer::Document::Dirty
38
44
  result
39
45
  end
40
46
 
41
- def attribute_may_change(attr, current_value)
47
+ def attribute_may_change(*args)
48
+ attr = args.first
49
+ current_value = begin
50
+ case args.size
51
+ when 1 then assert_access_field(attr); read_attribute(attr)
52
+ when 2 then args.last
53
+ else raise
54
+ end
55
+ rescue NoBrainer::Error::MissingAttribute => e
56
+ e
57
+ end
58
+
42
59
  unless @_old_attributes.has_key?(attr)
43
60
  @_old_attributes[attr] = current_value.deep_dup
44
61
  end
@@ -74,7 +91,7 @@ module NoBrainer::Document::Dirty
74
91
  end
75
92
 
76
93
  define_method("#{attr}=") do |value|
77
- attribute_may_change(attr, read_attribute(attr))
94
+ attribute_may_change(attr)
78
95
  super(value)
79
96
  end
80
97
  end
@@ -5,6 +5,7 @@ module NoBrainer::Document::DynamicAttributes
5
5
  if self.respond_to?("#{name}")
6
6
  super
7
7
  else
8
+ assert_access_field(name)
8
9
  @_attributes[name].tap { |value| attribute_may_change(name, value) if value.respond_to?(:size) }
9
10
  end
10
11
  end
@@ -13,8 +14,10 @@ module NoBrainer::Document::DynamicAttributes
13
14
  if self.respond_to?("#{name}=")
14
15
  super
15
16
  else
16
- attribute_may_change(name, read_attribute(name))
17
+ attribute_may_change(name)
17
18
  @_attributes[name] = value
19
+ clear_missing_field(name)
20
+ value
18
21
  end
19
22
  end
20
23
 
@@ -1,5 +1,5 @@
1
1
  module NoBrainer::Document::Index
2
- VALID_INDEX_OPTIONS = [:multi]
2
+ VALID_INDEX_OPTIONS = [:multi, :as]
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
@@ -25,11 +25,17 @@ module NoBrainer::Document::Index
25
25
  if name.in?(NoBrainer::Document::Attributes::RESERVED_FIELD_NAMES)
26
26
  raise "Cannot use a reserved field name: #{name}"
27
27
  end
28
+
28
29
  if has_field?(name) && kind != :single
29
30
  raise "Cannot reuse field name #{name}"
30
31
  end
31
32
 
32
- indexes[name] = {:kind => kind, :what => what, :options => options}
33
+ as = options.delete(:as)
34
+ as ||= fields[name][:as] if has_field?(name)
35
+ as ||= name
36
+ as = as.to_sym
37
+
38
+ indexes[name] = {:kind => kind, :what => what, :as => as, :options => options}
33
39
  end
34
40
 
35
41
  def remove_index(name)
@@ -40,6 +46,10 @@ module NoBrainer::Document::Index
40
46
  !!indexes[name.to_sym]
41
47
  end
42
48
 
49
+ def lookup_index_alias(attr)
50
+ indexes[attr.to_sym].try(:[], :as) || attr
51
+ end
52
+
43
53
  def _field(attr, options={})
44
54
  if has_index?(attr) && indexes[attr][:kind] != :single
45
55
  raise "Cannot reuse index attr #{attr}"
@@ -47,11 +57,12 @@ module NoBrainer::Document::Index
47
57
 
48
58
  super
49
59
 
60
+ as = {:as => options[:as]}
50
61
  case options[:index]
51
62
  when nil then
52
- when Hash then index(attr, options[:index])
53
- when Symbol then index(attr, options[:index] => true)
54
- when true then index(attr)
63
+ when Hash then index(attr, as.merge(options[:index]))
64
+ when Symbol then index(attr, as.merge(options[:index] => true))
65
+ when true then index(attr, as)
55
66
  when false then remove_index(attr)
56
67
  end
57
68
  end
@@ -66,23 +77,47 @@ module NoBrainer::Document::Index
66
77
  index_args = self.indexes[index_name]
67
78
 
68
79
  index_proc = case index_args[:kind]
69
- when :single then nil
70
- when :compound then ->(doc) { index_args[:what].map { |field| doc[field] } }
80
+ when :single then ->(doc) { doc[lookup_field_alias(index_name)] }
81
+ when :compound then ->(doc) { index_args[:what].map { |field| doc[lookup_field_alias(field)] } }
71
82
  when :proc then index_args[:what]
72
83
  end
73
84
 
74
- NoBrainer.run(self.rql_table.index_create(index_name, index_args[:options], &index_proc))
85
+ NoBrainer.run(self.rql_table.index_create(index_args[:as], index_args[:options], &index_proc))
75
86
  wait_for_index(index_name) unless options[:wait] == false
76
- STDERR.puts "Created index #{self}.#{index_name}" if options[:verbose]
87
+
88
+ if options[:verbose]
89
+ if index_name == index_args[:as]
90
+ STDERR.puts "Created index #{self}.#{index_name}"
91
+ else
92
+ STDERR.puts "Created index #{self}.#{index_name} as #{index_args[:as]}"
93
+ end
94
+ end
77
95
  end
78
96
 
79
97
  def perform_drop_index(index_name, options={})
80
- NoBrainer.run(self.rql_table.index_drop(index_name))
81
- STDERR.puts "Dropped index #{self}.#{index_name}" if options[:verbose]
98
+ aliased_name = self.indexes[index_name].try(:[], :as) || index_name
99
+ NoBrainer.run(self.rql_table.index_drop(aliased_name))
100
+
101
+ if options[:verbose]
102
+ if index_name == index_args[:as]
103
+ STDERR.puts "Dropped index #{self}.#{index_name}"
104
+ else
105
+ STDERR.puts "Dreated index #{self}.#{index_name} as #{index_args[:as]}"
106
+ end
107
+ end
108
+ end
109
+
110
+ def get_index_alias_reverse_map
111
+ Hash[self.indexes.map { |k,v| [v[:as], k] }].tap do |mapping|
112
+ raise "Detected clashing index aliases" if mapping.count != self.indexes.count
113
+ end
82
114
  end
83
115
 
84
116
  def perform_update_indexes(options={})
85
- current_indexes = NoBrainer.run(self.rql_table.index_list).map(&:to_sym)
117
+ alias_mapping = self.get_index_alias_reverse_map
118
+ current_indexes = NoBrainer.run(self.rql_table.index_list).map do |index|
119
+ alias_mapping[index.to_sym] || index.to_sym
120
+ end
86
121
  wanted_indexes = self.indexes.keys - [self.pk_name]
87
122
 
88
123
  (current_indexes - wanted_indexes).each do |index_name|
@@ -0,0 +1,72 @@
1
+ module NoBrainer::Document::LazyFetch
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ singleton_class.send(:attr_accessor, :fields_to_lazy_fetch)
6
+ self.fields_to_lazy_fetch = Set.new
7
+ end
8
+
9
+ def assign_attributes(attrs, options={})
10
+ if options[:lazy_fetch].present?
11
+ lazy_fetch = options[:lazy_fetch]
12
+ lazy_fetch = lazy_fetch.keys if lazy_fetch.is_a?(Hash)
13
+ @lazy_fetch = Set.new(lazy_fetch.map(&:to_s))
14
+ end
15
+ super
16
+ end
17
+
18
+ def reload(options={})
19
+ lazy_fetch = self.class.fields_to_lazy_fetch.to_a
20
+ return super unless lazy_fetch.present?
21
+ return super if options[:pluck]
22
+ super(options.merge(:without => lazy_fetch, :lazy_fetch => lazy_fetch))
23
+ end
24
+
25
+ module ClassMethods
26
+ def inherited(subclass)
27
+ subclass.fields_to_lazy_fetch = self.fields_to_lazy_fetch.dup
28
+ super
29
+ end
30
+
31
+ def _field(attr, options={})
32
+ super
33
+ attr = attr.to_s
34
+ klass = self
35
+ inject_in_layer :lazy_fetch do
36
+ if options[:lazy_fetch]
37
+ klass.for_each_subclass { |_klass| _klass.fields_to_lazy_fetch << attr }
38
+ else
39
+ klass.for_each_subclass { |_klass| _klass.fields_to_lazy_fetch.delete(attr) }
40
+ end
41
+
42
+ # Lazy loading can also specified through criteria.
43
+ define_method("#{attr}") do
44
+ return super() unless @lazy_fetch
45
+
46
+ begin
47
+ super()
48
+ rescue NoBrainer::Error::MissingAttribute => e
49
+ raise e unless attr.in?(@lazy_fetch)
50
+ reload(:pluck => attr, :keep_ivars => true)
51
+ @lazy_fetch.delete(attr)
52
+ retry
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ def _remove_field(attr, options={})
59
+ super
60
+ for_each_subclass { |klass| klass.fields_to_lazy_fetch.delete(attr) }
61
+ inject_in_layer :lazy_fetch do
62
+ remove_method("#{attr}") if method_defined?("#{attr}")
63
+ end
64
+ end
65
+
66
+ def all
67
+ criteria = super
68
+ criteria = criteria.lazy_fetch(*self.fields_to_lazy_fetch) if self.fields_to_lazy_fetch.present?
69
+ criteria
70
+ end
71
+ end
72
+ end