nobrainer 0.15.0 → 0.16.0

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