elastic_record 0.8.2 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/elastic_record.gemspec +1 -1
  2. data/lib/elastic_record/index/deferred.rb +81 -0
  3. data/lib/elastic_record/index.rb +6 -4
  4. data/lib/elastic_record/model.rb +3 -1
  5. data/lib/elastic_record/relation/merging.rb +6 -5
  6. data/lib/elastic_record/relation/search_methods.rb +8 -8
  7. data/lib/elastic_record/searches_many/association.rb +134 -0
  8. data/lib/elastic_record/searches_many/autosave.rb +72 -0
  9. data/lib/elastic_record/searches_many/builder.rb +42 -0
  10. data/lib/elastic_record/searches_many/collection_proxy.rb +20 -0
  11. data/lib/elastic_record/searches_many/reflection.rb +41 -0
  12. data/lib/elastic_record/searches_many.rb +91 -0
  13. data/lib/elastic_record.rb +3 -1
  14. data/test/elastic_record/callbacks_test.rb +2 -7
  15. data/test/elastic_record/config_test.rb +1 -1
  16. data/test/elastic_record/index/documents_test.rb +1 -1
  17. data/test/elastic_record/index/manage_test.rb +5 -1
  18. data/test/elastic_record/index/percolator_test.rb +5 -0
  19. data/test/elastic_record/relation/batches_test.rb +1 -3
  20. data/test/elastic_record/relation/delegation_test.rb +0 -5
  21. data/test/elastic_record/relation/finder_methods_test.rb +2 -4
  22. data/test/elastic_record/relation/none_test.rb +0 -5
  23. data/test/elastic_record/relation/search_methods_test.rb +19 -0
  24. data/test/elastic_record/relation_test.rb +0 -7
  25. data/test/elastic_record/searches_many/autosave_test.rb +31 -0
  26. data/test/elastic_record/searches_many/collection_proxy_test.rb +23 -0
  27. data/test/elastic_record/searches_many/reflection_test.rb +20 -0
  28. data/test/elastic_record/searches_many_test.rb +65 -0
  29. data/test/helper.rb +16 -1
  30. data/test/support/connect.rb +1 -1
  31. data/test/support/models/test_model.rb +98 -0
  32. data/test/support/models/warehouse.rb +6 -0
  33. data/test/support/{widget.rb → models/widget.rb} +10 -19
  34. metadata +16 -3
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'elastic_record'
5
- s.version = '0.8.2'
5
+ s.version = '0.9.0'
6
6
  s.summary = 'Use Elastic Search with your objects'
7
7
  s.description = 'Find your records with elastic search'
8
8
 
@@ -0,0 +1,81 @@
1
+ module ElasticRecord
2
+ class Index
3
+ module Deferred
4
+ class DeferredConnection
5
+ class DeferredAction < Struct.new(:method, :args, :block)
6
+ def run(index)
7
+ index.send(method, *args, &block)
8
+ end
9
+ end
10
+
11
+ attr_accessor :index
12
+ attr_accessor :deferred_actions
13
+ attr_accessor :writes_made
14
+
15
+ def initialize(index)
16
+ self.index = index
17
+ reset!
18
+ end
19
+
20
+ def reset!
21
+ if writes_made
22
+ begin
23
+ index.disable_deferring!
24
+ index.reset
25
+ ensure
26
+ index.enable_deferring!
27
+ end
28
+ end
29
+ self.deferred_actions = []
30
+ self.writes_made = false
31
+ end
32
+
33
+ def flush!
34
+ deferred_actions.each do |queued_action|
35
+ self.writes_made = true
36
+ queued_action.run(index.real_connection)
37
+ end
38
+ deferred_actions.clear
39
+ end
40
+
41
+ private
42
+ READ_METHODS = [:json_get, :head]
43
+ def method_missing(method, *args, &block)
44
+ super unless index.real_connection.respond_to?(method)
45
+
46
+ if READ_METHODS.include?(method)
47
+ flush!
48
+ index.real_connection.json_post "/#{index.alias_name}/_refresh"
49
+ index.real_connection.send(method, *args, &block)
50
+ else
51
+ deferred_actions << DeferredAction.new(method, args, block)
52
+ end
53
+ end
54
+ end
55
+
56
+ def enable_deferring!
57
+ @deferring_enabled = true
58
+ end
59
+
60
+ def disable_deferring!
61
+ @deferring_enabled = false
62
+ end
63
+
64
+ def connection
65
+ if @deferring_enabled
66
+ deferred_connection
67
+ else
68
+ real_connection
69
+ end
70
+ end
71
+
72
+ def reset_deferring!
73
+ deferred_connection.reset!
74
+ end
75
+
76
+ def deferred_connection
77
+ @deferred_connection ||= DeferredConnection.new(self)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -1,3 +1,4 @@
1
+ require 'elastic_record/index/deferred'
1
2
  require 'elastic_record/index/documents'
2
3
  require 'elastic_record/index/manage'
3
4
  require 'elastic_record/index/mapping'
@@ -10,6 +11,7 @@ module ElasticRecord
10
11
  include Manage
11
12
  include Mapping
12
13
  include Percolator
14
+ include Deferred
13
15
 
14
16
  attr_accessor :model
15
17
  attr_accessor :disabled
@@ -35,13 +37,13 @@ module ElasticRecord
35
37
  @disabled = false
36
38
  end
37
39
 
40
+ def real_connection
41
+ model.elastic_connection
42
+ end
43
+
38
44
  private
39
45
  def new_index_name
40
46
  "#{alias_name}_#{Time.now.to_i}"
41
47
  end
42
-
43
- def connection
44
- model.elastic_connection
45
- end
46
48
  end
47
49
  end
@@ -2,7 +2,9 @@ module ElasticRecord
2
2
  module Model
3
3
  def self.included(base)
4
4
  base.class_eval do
5
- extend Searching, ClassMethods
5
+ extend Searching
6
+ extend ClassMethods
7
+ include SearchesMany
6
8
  end
7
9
  end
8
10
 
@@ -18,16 +18,17 @@ module ElasticRecord
18
18
  @values = other.values
19
19
  end
20
20
 
21
- def normal_values
22
- Relation::MULTI_VALUE_METHODS + Relation::SINGLE_VALUE_METHODS
23
- end
24
-
25
21
  def merge
26
- normal_values.each do |name|
22
+ Relation::SINGLE_VALUE_METHODS.each do |name|
27
23
  value = values[name]
28
24
  relation.send("#{name}!", value) unless value.blank?
29
25
  end
30
26
 
27
+ Relation::MULTI_VALUE_METHODS.each do |name|
28
+ value = values[name]
29
+ relation.send("#{name}!", *value) unless value.blank?
30
+ end
31
+
31
32
  relation
32
33
  end
33
34
  end
@@ -35,7 +35,7 @@ module ElasticRecord
35
35
  end
36
36
 
37
37
  def filter!(*args)
38
- self.filter_values += args.flatten
38
+ self.filter_values += args
39
39
  self
40
40
  end
41
41
 
@@ -137,9 +137,9 @@ module ElasticRecord
137
137
  if query && filter
138
138
  arelastic.query.filtered(query, filter)
139
139
  elsif query
140
- query
140
+ Arelastic::Searches::Query.new(query)
141
141
  elsif filter
142
- arelastic.query.constant_score(filter)
142
+ arelastic.query.constant_score(Arelastic::Searches::Filter.new(filter))
143
143
  else
144
144
  arelastic.query.match_all
145
145
  end
@@ -150,9 +150,7 @@ module ElasticRecord
150
150
  query = Arelastic::Queries::QueryString.new query
151
151
  end
152
152
 
153
- if query
154
- Arelastic::Searches::Query.new query
155
- end
153
+ query
156
154
  end
157
155
 
158
156
  def build_filter(filters)
@@ -161,6 +159,8 @@ module ElasticRecord
161
159
  filters.map do |filter|
162
160
  if filter.is_a?(Arelastic::Filters::Filter)
163
161
  nodes << filter
162
+ elsif filter.is_a?(ElasticRecord::Relation)
163
+ nodes << Arelastic::Filters::HasChild.new(filter.elastic_index.type, filter.as_elastic['query'])
164
164
  else
165
165
  filter.each do |field, terms|
166
166
  case terms
@@ -176,9 +176,9 @@ module ElasticRecord
176
176
  end
177
177
 
178
178
  if nodes.size == 1
179
- Arelastic::Searches::Filter.new nodes.first
179
+ nodes.first
180
180
  elsif nodes.size > 1
181
- Arelastic::Searches::Filter.new Arelastic::Filters::And.new(nodes)
181
+ Arelastic::Filters::And.new(nodes)
182
182
  end
183
183
  end
184
184
 
@@ -0,0 +1,134 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
3
+ module ElasticRecord
4
+ module SearchesMany
5
+ class Association
6
+ attr_reader :owner, :reflection, :collection
7
+
8
+ delegate :klass, :options, to: :reflection
9
+
10
+ def initialize(owner, reflection)
11
+ @owner = owner
12
+ @reflection = reflection
13
+ @collection = []
14
+ @loaded = false
15
+ end
16
+
17
+ def writer(other_records)
18
+ other_records = other_records.map do |other_record|
19
+ other_record.is_a?(Hash) ? klass.new(other_record) : other_record
20
+ end
21
+
22
+ if reflection.counter_cache_column
23
+ owner.send("#{reflection.counter_cache_column}=", other_records.size)
24
+ end
25
+
26
+ if reflection.touch_column
27
+ owner.send("#{reflection.touch_column}=", Time.current)
28
+ end
29
+
30
+ delete(load_collection - other_records)
31
+ merge_collections(load_collection, other_records)
32
+ concat(other_records - load_collection)
33
+ end
34
+
35
+ def reader
36
+ CollectionProxy.new(self)
37
+ end
38
+
39
+ def loaded?
40
+ @loaded
41
+ end
42
+
43
+ def concat(*records)
44
+ load_collection if owner.new_record?
45
+
46
+ result = true
47
+
48
+ records.flatten.each do |record|
49
+ add_to_collection(record) do |r|
50
+ result &&= record.save unless owner.new_record?
51
+ end
52
+ end
53
+
54
+ result && records
55
+ end
56
+
57
+ def delete(records)
58
+ if options[:autosave] || owner.new_record?
59
+ records.each(&:mark_for_destruction)
60
+ else
61
+ record.destroy
62
+ end
63
+ end
64
+
65
+ def scope
66
+ search = klass.elastic_search.filter "#{reflection.belongs_to}_id" => owner.id
67
+ if options[:as]
68
+ search.filter! "#{reflection.belongs_to}_type" => owner.class.name
69
+ end
70
+ search
71
+ end
72
+
73
+ def load_collection
74
+ unless @loaded
75
+ @collection = merge_collections(persisted_collection, collection)
76
+ @loaded = true
77
+ end
78
+
79
+ loaded = true
80
+ collection
81
+ end
82
+
83
+ private
84
+ def load_persisted_collection?
85
+ !loaded? || owner.new_record?
86
+ end
87
+
88
+ def persisted_collection
89
+ scope.to_a
90
+ end
91
+
92
+ def merge_collections(existing_records, new_records)
93
+ return existing_records if new_records.empty?
94
+ return new_records if existing_records.empty?
95
+
96
+ existing_records.map! do |existing_record|
97
+ if new_record = new_records.delete(existing_record)
98
+ (existing_record.attributes.keys - new_record.changes.keys).each do |name|
99
+ new_record.send("#{name}=", existing_record.send(name))
100
+ end
101
+
102
+ new_record
103
+ else
104
+ existing_record
105
+ end
106
+ end
107
+
108
+ existing_records + new_records
109
+ end
110
+
111
+ def add_to_collection(record)
112
+ callback(:before_add, record)
113
+
114
+ record.send("#{reflection.belongs_to}=", owner)
115
+ yield(record) if block_given?
116
+ @collection << record
117
+
118
+ callback(:after_add, record)
119
+
120
+ record
121
+ end
122
+
123
+ def callback(method, record)
124
+ reflection.callbacks[method].each do |callback|
125
+ if callback.is_a?(Symbol)
126
+ owner.send(callback, record)
127
+ else
128
+ callback.call(owner, record)
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,72 @@
1
+ module ElasticRecord
2
+ module SearchesMany
3
+ module Autosave
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ def add_autosave_callbacks(reflection)
8
+ add_autosave_after_save_callbacks(reflection)
9
+ add_autosave_validation_callbacks(reflection)
10
+ end
11
+
12
+ def add_autosave_after_save_callbacks(reflection)
13
+ before_save { save_autosave_records(reflection) }
14
+ end
15
+
16
+ def add_autosave_validation_callbacks(reflection)
17
+ validate { validate_autosave_records(reflection) }
18
+ end
19
+ end
20
+ end
21
+
22
+ def save_autosave_records(reflection)
23
+ if association = searches_many_instance_get(reflection.name)
24
+ associated_records_to_autosave(association).each do |record|
25
+ if record.marked_for_destruction?
26
+ record.destroy
27
+ elsif record.changed?
28
+ record.save
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ def validate_autosave_records(reflection)
35
+ if association = searches_many_instance_get(reflection.name)
36
+ associated_records_to_autosave(association).each do |record|
37
+ unless record.valid?
38
+ record.errors.each do |attribute, message|
39
+ attribute = "#{reflection.name}.#{attribute}"
40
+ errors[attribute] << message
41
+ errors[attribute].uniq!
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ def associated_records_to_autosave(association)
49
+ if association.loaded?
50
+ association.load_collection
51
+ else
52
+ []
53
+ end
54
+ end
55
+
56
+ # Marks this record to be destroyed as part of the parents save transaction.
57
+ # This does _not_ actually destroy the record instantly, rather child record will be destroyed
58
+ # when <tt>parent.save</tt> is called.
59
+ #
60
+ # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
61
+ def mark_for_destruction
62
+ @marked_for_destruction = true
63
+ end
64
+
65
+ # Returns whether or not this record will be destroyed as part of the parents save transaction.
66
+ #
67
+ # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
68
+ def marked_for_destruction?
69
+ @marked_for_destruction
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,42 @@
1
+ module ElasticRecord
2
+ module SearchesMany
3
+ class Builder
4
+ def self.build(model, name, options)
5
+ new(model, name, options).build
6
+ end
7
+
8
+ attr_reader :model, :name, :options
9
+ def initialize(model, name, options)
10
+ @model, @name, @options = model, name, options
11
+ end
12
+
13
+ def build
14
+ define_writer
15
+ define_reader
16
+
17
+ reflection = ElasticRecord::SearchesMany::Reflection.new(model, name, options)
18
+ model.searches_many_reflections = model.searches_many_reflections.merge(name => reflection)
19
+
20
+ model.add_autosave_callbacks(reflection) # if options[:autosave]
21
+ end
22
+
23
+ def mixin
24
+ model.generated_searches_many_methods
25
+ end
26
+
27
+ def define_writer
28
+ name = self.name
29
+ mixin.redefine_method("#{name}=") do |records|
30
+ searches_many_association(name).writer(records)
31
+ end
32
+ end
33
+
34
+ def define_reader
35
+ name = self.name
36
+ mixin.redefine_method(name) do
37
+ searches_many_association(name).reader
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,20 @@
1
+ module ElasticRecord
2
+ module SearchesMany
3
+ class CollectionProxy < ElasticRecord::Relation
4
+ def initialize(association)
5
+ @association = association
6
+ super association.klass, association.klass.arelastic
7
+ merge! association.scope
8
+ end
9
+
10
+ def to_a
11
+ @association.load_collection.reject(&:destroyed?)
12
+ end
13
+
14
+ def <<(*records)
15
+ @association.concat(records) && self
16
+ end
17
+ alias_method :push, :<<
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,41 @@
1
+ module ElasticRecord
2
+ module SearchesMany
3
+ class Reflection
4
+ attr_reader :model, :name, :options
5
+ attr_reader :callbacks
6
+ def initialize(model, name, options)
7
+ @model, @name, @options = model, name, options
8
+ @callbacks = define_callbacks(options)
9
+ end
10
+
11
+ def klass
12
+ klass_name.constantize
13
+ end
14
+
15
+ def klass_name
16
+ name.to_s.classify
17
+ end
18
+
19
+ def belongs_to
20
+ options[:as] ? options[:as].to_s : model.name.to_s.demodulize.underscore
21
+ end
22
+
23
+ CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
24
+ def define_callbacks(options)
25
+ Hash[CALLBACKS.map { |callback_name| [callback_name, Array(options[callback_name.to_sym])] }]
26
+ end
27
+
28
+ def touch_column
29
+ if options[:touch]
30
+ options[:touch] == true ? :updated_at : options[:touch].to_sym
31
+ end
32
+ end
33
+
34
+ def counter_cache_column
35
+ if options[:counter_cache]
36
+ (options[:counter_cache] == true ? "#{name}_count" : options[:counter_cache]).to_sym
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,91 @@
1
+ require 'elastic_record/searches_many/association'
2
+ require 'elastic_record/searches_many/autosave'
3
+ require 'elastic_record/searches_many/builder'
4
+ require 'elastic_record/searches_many/collection_proxy'
5
+ require 'elastic_record/searches_many/reflection'
6
+
7
+ module ElasticRecord
8
+ module SearchesMany
9
+ def self.included(base)
10
+ base.class_eval do
11
+ extend ClassMethods
12
+
13
+ class_attribute :searches_many_reflections
14
+ self.searches_many_reflections = {}
15
+
16
+ include ElasticRecord::SearchesMany::Autosave
17
+ end
18
+ end
19
+
20
+ module ClassMethods
21
+ # Specifies a one-to-many association. The following methods for retrieval and query of
22
+ # collections of associated objects will be added:
23
+ #
24
+ # [collection]
25
+ # Returns an array of all the associated objects.
26
+ # [collection=objects]
27
+ # Replaces the collections content by deleting and adding objects as appropriate.
28
+ # [collection_params=objects]
29
+ # Support for nested assignment from a form
30
+ # === Options
31
+ # [:as]
32
+ # Specifies a polymorphic interface (See <tt>belongs_to</tt>).
33
+ # [:touch]
34
+ # Specify to update the owner when changed. Specify <tt>true</tt>
35
+ # to update the updated_at field. If you specify a symbol, that attribute
36
+ # will be updated with the current time in addition to the updated_at/on attribute.
37
+ # [:autosave]
38
+ # If true, always save the associated objects or destroy them if marked for destruction, when
39
+ # saving the parent object.
40
+ # [:counter_cache]
41
+ # Caches the number of belonging objects on the associate class. This requires that a column
42
+ # named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class)
43
+ # is used on the associate class (such as a Post class). You can also specify a custom counter
44
+ # cache column by providing a column name instead of a +true+/+false+ value to this
45
+ # option (e.g., <tt>:counter_cache => :my_custom_counter</tt>.)
46
+ #
47
+ # === Example
48
+ #
49
+ # Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
50
+ # * <tt>Firm#clients</tt>
51
+ # * <tt>Firm#clients=(objects)</tt>
52
+ # * <tt>Firm#client_params=(params)</tt>
53
+ def searches_many(name, options = {})
54
+ ElasticRecord::SearchesMany::Builder.build(self, name, options)
55
+ end
56
+
57
+ def generated_searches_many_methods
58
+ @generated_searches_many_methods ||= begin
59
+ mod = const_set(:GeneratedSearchesManyMethods, Module.new)
60
+ include mod
61
+ mod
62
+ end
63
+ end
64
+ end
65
+
66
+ # Returns the searches_many instance for the given name, instantiating it if it doesn't already exist
67
+ def searches_many_association(name)
68
+ association = searches_many_instance_get(name)
69
+
70
+ if association.nil?
71
+ association = ElasticRecord::SearchesMany::Association.new(self, searches_many_reflections[name])
72
+ searches_many_instance_set(name, association)
73
+ end
74
+
75
+ association
76
+ end
77
+
78
+ private
79
+ def searches_many_cache
80
+ @searches_many_cache ||= {}
81
+ end
82
+
83
+ def searches_many_instance_get(name)
84
+ searches_many_cache[name.to_sym]
85
+ end
86
+
87
+ def searches_many_instance_set(name, association)
88
+ searches_many_cache[name.to_sym] = association
89
+ end
90
+ end
91
+ end
@@ -1,5 +1,6 @@
1
1
  require 'arelastic'
2
2
  require 'active_support/core_ext/object/blank' # required because ActiveModel depends on this but does not require it
3
+ require 'active_support/concern'
3
4
  require 'active_model'
4
5
 
5
6
  module ElasticRecord
@@ -10,6 +11,7 @@ module ElasticRecord
10
11
  autoload :Lucene, 'elastic_record/lucene'
11
12
  autoload :Model, 'elastic_record/model'
12
13
  autoload :Relation, 'elastic_record/relation'
14
+ autoload :SearchesMany, 'elastic_record/searches_many'
13
15
  autoload :Searching, 'elastic_record/searching'
14
16
 
15
17
  class << self
@@ -19,4 +21,4 @@ module ElasticRecord
19
21
  end
20
22
  end
21
23
 
22
- require 'elastic_record/railtie' if defined?(Rails)
24
+ require 'elastic_record/railtie' if defined?(Rails)
@@ -1,16 +1,11 @@
1
1
  require 'helper'
2
2
 
3
3
  class ElasticRecord::CallbacksTest < MiniTest::Spec
4
- def setup
5
- super
6
- Widget.elastic_index.reset
7
- end
8
-
9
4
  def test_added_to_index
10
5
  widget = Widget.new id: '10', color: 'green'
11
6
  refute Widget.elastic_index.record_exists?(widget.id)
12
7
 
13
- widget.run_callbacks :save
8
+ widget.save
14
9
 
15
10
  assert Widget.elastic_index.record_exists?(widget.id)
16
11
  end
@@ -21,7 +16,7 @@ class ElasticRecord::CallbacksTest < MiniTest::Spec
21
16
 
22
17
  assert Widget.elastic_index.record_exists?(widget.id)
23
18
 
24
- widget.run_callbacks :destroy
19
+ widget.destroy
25
20
 
26
21
  refute Widget.elastic_index.record_exists?(widget.id)
27
22
  end
@@ -4,6 +4,6 @@ class ElasticRecord::ConfigTest < MiniTest::Spec
4
4
  def test_models
5
5
  ElasticRecord::Config.model_names = %w(Widget)
6
6
 
7
- assert_equal [Widget], ElasticRecord::Config.models
7
+ assert_equal [Warehouse, Widget], ElasticRecord::Config.models
8
8
  end
9
9
  end
@@ -3,7 +3,7 @@ require 'helper'
3
3
  class ElasticRecord::Index::DocumentsTest < MiniTest::Spec
4
4
  def setup
5
5
  super
6
-
6
+ index.disable_deferring!
7
7
  index.reset
8
8
  end
9
9
 
@@ -1,9 +1,13 @@
1
1
  require 'helper'
2
2
 
3
+ # class Cat
4
+ # include TestModel
5
+ # end
6
+
3
7
  class ElasticRecord::Index::ManageTest < MiniTest::Spec
4
8
  def setup
5
9
  super
6
-
10
+ index.disable_deferring!
7
11
  index.delete_all
8
12
  end
9
13
 
@@ -1,6 +1,11 @@
1
1
  require 'helper'
2
2
 
3
3
  class ElasticRecord::Index::PercolatorTest < MiniTest::Spec
4
+ def setup
5
+ super
6
+ index.disable_deferring!
7
+ end
8
+
4
9
  def test_create_percolator
5
10
  index.delete(index.percolator_index_name) if index.exists?(index.percolator_index_name)
6
11
 
@@ -2,7 +2,7 @@ require 'helper'
2
2
 
3
3
  class ElasticRecord::Relation::BatchesTest < MiniTest::Spec
4
4
  def setup
5
- Widget.elastic_index.reset
5
+ super
6
6
  create_widgets
7
7
  end
8
8
 
@@ -37,7 +37,5 @@ class ElasticRecord::Relation::BatchesTest < MiniTest::Spec
37
37
  Widget.new(id: 10, color: 'blue'),
38
38
  Widget.new(id: 15, color: 'green'),
39
39
  ]
40
-
41
- Widget.elastic_index.refresh
42
40
  end
43
41
  end
@@ -1,13 +1,8 @@
1
1
  require 'helper'
2
2
 
3
3
  class ElasticRecord::Relation::DelegationTest < MiniTest::Spec
4
- def setup
5
- Widget.elastic_index.reset
6
- end
7
-
8
4
  def test_delegate_to_array
9
5
  Widget.elastic_index.index_document('5', color: 'red')
10
- Widget.elastic_index.refresh
11
6
 
12
7
  records = []
13
8
  Widget.elastic_relation.each do |record|
@@ -2,7 +2,7 @@ require 'helper'
2
2
 
3
3
  class ElasticRecord::Relation::FinderMethodsTest < MiniTest::Spec
4
4
  def setup
5
- Widget.elastic_index.reset
5
+ super
6
6
  create_widgets
7
7
  end
8
8
 
@@ -37,8 +37,6 @@ class ElasticRecord::Relation::FinderMethodsTest < MiniTest::Spec
37
37
  Widget.elastic_index.bulk_add [
38
38
  Widget.new(color: 'red', id: '05'),
39
39
  Widget.new(color: 'blue', id: '10'),
40
- ]
41
-
42
- Widget.elastic_index.refresh
40
+ ]
43
41
  end
44
42
  end
@@ -1,11 +1,6 @@
1
1
  require 'helper'
2
2
 
3
3
  class ElasticRecord::Relation::NoneTest < MiniTest::Spec
4
- def setup
5
- super
6
- Widget.elastic_index.reset
7
- end
8
-
9
4
  def test_none
10
5
  none = Widget.elastic_relation.none
11
6
 
@@ -57,6 +57,25 @@ class ElasticRecord::Relation::SearchMethodsTest < MiniTest::Spec
57
57
  assert_equal expected, relation.as_elastic['query']
58
58
  end
59
59
 
60
+ def test_filter_with_another_relation
61
+ relation.filter! Widget.elastic_search.query('red')
62
+
63
+ expected = {
64
+ "constant_score" => {
65
+ "filter" => {
66
+ "has_child" => {
67
+ "type" => "widget",
68
+ "query" => {
69
+ "query_string" => {"query"=>"red"}
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ assert_equal expected, relation.as_elastic['query']
77
+ end
78
+
60
79
  def test_query_with_only_query
61
80
  relation.query!('foo')
62
81
 
@@ -1,11 +1,6 @@
1
1
  require 'helper'
2
2
 
3
3
  class ElasticRecord::RelationTest < MiniTest::Spec
4
- def setup
5
- super
6
- Widget.elastic_index.reset
7
- end
8
-
9
4
  def test_count
10
5
  create_widgets [Widget.new(id: 5, color: 'red'), Widget.new(id: 10, color: 'blue')]
11
6
 
@@ -32,7 +27,6 @@ class ElasticRecord::RelationTest < MiniTest::Spec
32
27
  create_widgets [Widget.new(id: 10, color: 'blue')]
33
28
 
34
29
  # explain = Widget.elastic_relation.filter(color: 'blue').explain('10')
35
- # p "explain = #{explain}"
36
30
  end
37
31
 
38
32
  def test_to_hits
@@ -70,6 +64,5 @@ class ElasticRecord::RelationTest < MiniTest::Spec
70
64
  private
71
65
  def create_widgets(widgets)
72
66
  Widget.elastic_index.bulk_add(widgets)
73
- Widget.elastic_index.refresh
74
67
  end
75
68
  end
@@ -0,0 +1,31 @@
1
+ require 'helper'
2
+
3
+ class ElasticRecord::SearchesMany::AutosaveTest < MiniTest::Spec
4
+ def test_save_associations_callback
5
+ warehouse = Warehouse.new
6
+ widget = Widget.new
7
+ warehouse.widgets = [widget]
8
+ assert widget.new_record?
9
+
10
+ warehouse.save
11
+
12
+ assert widget.persisted?
13
+ end
14
+
15
+ def test_validate_associations_callback
16
+ warehouse = Warehouse.new
17
+ widget = Widget.new color: 123
18
+ warehouse.widgets = [widget]
19
+
20
+ assert warehouse.invalid?
21
+ assert_equal ["is invalid"], warehouse.errors['widgets.color']
22
+ end
23
+
24
+ def test_mark_for_destruction
25
+ widget = Widget.new
26
+
27
+ widget.mark_for_destruction
28
+
29
+ assert widget.marked_for_destruction?
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ require 'helper'
2
+
3
+ class ElasticRecord::SearchesMany::CollectionProxyTest < MiniTest::Spec
4
+ def test_add_to_new_record
5
+ warehouse = Warehouse.new
6
+ widget = Widget.new
7
+
8
+ warehouse.widgets << widget
9
+
10
+ assert widget.new_record?
11
+ assert_equal [widget], warehouse.widgets
12
+ end
13
+
14
+ def test_add_to_persisted_record
15
+ warehouse = Warehouse.create
16
+ widget = Widget.new
17
+
18
+ warehouse.widgets << widget
19
+
20
+ assert !widget.new_record?
21
+ assert_equal [widget], warehouse.widgets
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ require 'helper'
2
+
3
+ class ElasticRecord::SearchesMany::ReflectionTest < MiniTest::Spec
4
+ def test_touch_column
5
+ assert_nil reflection_class.new(Warehouse, :widgets, {}).touch_column
6
+ assert_equal :updated_at, reflection_class.new(Warehouse, :widgets, touch: true).touch_column
7
+ assert_equal :my_column, reflection_class.new(Warehouse, :widgets, touch: :my_column).touch_column
8
+ end
9
+
10
+ def test_counter_cache_column
11
+ assert_nil reflection_class.new(Warehouse, :widgets, {}).counter_cache_column
12
+ assert_equal :widgets_count, reflection_class.new(Warehouse, :widgets, counter_cache: true).counter_cache_column
13
+ assert_equal :my_column, reflection_class.new(Warehouse, :widgets, counter_cache: :my_column).counter_cache_column
14
+ end
15
+
16
+ private
17
+ def reflection_class
18
+ ElasticRecord::SearchesMany::Reflection
19
+ end
20
+ end
@@ -0,0 +1,65 @@
1
+ require 'helper'
2
+
3
+ class ElasticRecord::SearchesManyTest < MiniTest::Spec
4
+ def test_reader
5
+ warehouse = Warehouse.create
6
+ related_widget = Widget.create warehouse: warehouse
7
+ unrelated_widget = Widget.create
8
+
9
+ assert_equal [related_widget], warehouse.widgets
10
+ end
11
+
12
+ def test_write_with_objects
13
+ warehouse = Warehouse.new
14
+ widget = Widget.new
15
+
16
+ warehouse.widgets = [widget]
17
+
18
+ assert widget.new_record?
19
+ assert_equal warehouse.id, widget.warehouse_id
20
+ # assert_equal 1, warehouse.widgets_count
21
+ # assert_in_delta Time.current, warehouse.widgets_updated_at, 5
22
+ end
23
+
24
+ def test_write_with_attributes
25
+ warehouse = Warehouse.new
26
+
27
+ warehouse.widgets = [
28
+ {
29
+ color: 'blue',
30
+ name: 'Toy'
31
+ }
32
+ ]
33
+
34
+ widgets = warehouse.widgets
35
+ assert_equal 1, widgets.size
36
+ end
37
+
38
+ def test_write_marks_destroyed
39
+ warehouse = Warehouse.new
40
+ widget = Widget.create warehouse: warehouse
41
+
42
+ warehouse.widgets = []
43
+
44
+ association = warehouse.searches_many_association(:widgets)
45
+ assert_equal 1, association.reader.size
46
+ assert association.reader.first.marked_for_destruction?
47
+ end
48
+
49
+ def test_write_existing_record
50
+ widget = Widget.create name: 'Toy', color: 'green'
51
+ warehouse = Warehouse.new widgets: [widget]
52
+
53
+ warehouse.widgets = [
54
+ {
55
+ id: widget.id,
56
+ color: "blue"
57
+ }
58
+ ]
59
+
60
+ widgets = warehouse.widgets
61
+ assert_equal 1, widgets.size
62
+ assert_equal "blue", widgets.first.color
63
+ assert_equal "Toy", widgets.first.name
64
+ end
65
+ end
data/test/helper.rb CHANGED
@@ -3,8 +3,13 @@ Bundler.require
3
3
 
4
4
  require 'minitest/autorun'
5
5
 
6
- require 'support/widget'
7
6
  require 'support/connect'
7
+ require 'support/models/test_model'
8
+ require 'support/models/warehouse'
9
+ require 'support/models/widget'
10
+ Widget.elastic_index.reset
11
+
12
+ ElasticRecord::Config.model_names = %w(Warehouse Widget)
8
13
 
9
14
  FakeWeb.allow_net_connect = %r[^https?://127.0.0.1]
10
15
 
@@ -13,6 +18,16 @@ module MiniTest
13
18
  def setup
14
19
  super
15
20
  FakeWeb.clean_registry
21
+
22
+ ElasticRecord::Config.models.each do |model|
23
+ model.elastic_index.enable_deferring!
24
+ end
25
+ end
26
+
27
+ def teardown
28
+ ElasticRecord::Config.models.each do |model|
29
+ model.elastic_index.reset_deferring!
30
+ end
16
31
  end
17
32
  end
18
33
  end
@@ -1 +1 @@
1
- ElasticRecord::Config.servers = '127.0.0.1:9200'
1
+ ElasticRecord::Config.servers = '127.0.0.1:9200'
@@ -0,0 +1,98 @@
1
+ module TestModel
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ extend ActiveModel::Naming
6
+ extend ActiveModel::Callbacks
7
+ define_model_callbacks :save, :destroy
8
+
9
+ include ActiveModel::Dirty
10
+ include ActiveModel::Validations
11
+
12
+ include ElasticRecord::Model
13
+ include ElasticRecord::Callbacks
14
+ end
15
+
16
+ module ClassMethods
17
+ def find(ids)
18
+ ids.map { |id| new(id: id, color: 'red') }
19
+ end
20
+
21
+ def base_class
22
+ self
23
+ end
24
+
25
+ def create(attributes = {})
26
+ record = new(attributes)
27
+ record.save
28
+ record
29
+ end
30
+
31
+ def define_attributes(attributes)
32
+ define_attribute_methods attributes
33
+
34
+ attributes.each do |attribute|
35
+ define_method attribute do
36
+ instance_variable_get("@#{attribute}")
37
+ end
38
+
39
+ define_method "#{attribute}=" do |value|
40
+ send("#{attribute}_will_change!")
41
+ instance_variable_set("@#{attribute}", value)
42
+ end
43
+ end
44
+
45
+ define_method 'attributes' do
46
+ Hash[attributes.map { |attr| [attr.to_s, send(attr)] }]
47
+ end
48
+ end
49
+ end
50
+
51
+ def initialize(attrs = {})
52
+ self.attributes = attrs
53
+ end
54
+
55
+ def attributes=(attrs)
56
+ attrs.each do |key, val|
57
+ send("#{key}=", val)
58
+ end
59
+ end
60
+
61
+ def id=(value)
62
+ @id = value
63
+ end
64
+
65
+ def id
66
+ @id ||= rand(10000).to_s
67
+ end
68
+
69
+ def save
70
+ @persisted = true
71
+ run_callbacks :save
72
+ end
73
+
74
+ def destroy
75
+ @destroyed = true
76
+ run_callbacks :destroy
77
+ end
78
+
79
+ def ==(other)
80
+ id == other.id
81
+ end
82
+
83
+ def changed?
84
+ true
85
+ end
86
+
87
+ def new_record?
88
+ !@persisted
89
+ end
90
+
91
+ def persisted?
92
+ @persisted
93
+ end
94
+
95
+ def destroyed?
96
+ @destroyed
97
+ end
98
+ end
@@ -0,0 +1,6 @@
1
+ class Warehouse
2
+ include TestModel
3
+
4
+ define_attributes [:name]
5
+ searches_many :widgets
6
+ end
@@ -1,10 +1,9 @@
1
1
  class Widget
2
- extend ActiveModel::Naming
3
- extend ActiveModel::Callbacks
4
- define_model_callbacks :save, :destroy
2
+ include TestModel
5
3
 
6
- include ElasticRecord::Model
7
- include ElasticRecord::Callbacks
4
+ validates :color, format: {with: /[a-z]/}
5
+
6
+ define_attributes [:name, :color, :warehouse_id]
8
7
 
9
8
  self.elastic_index.mapping[:properties].update(
10
9
  name: {
@@ -16,14 +15,13 @@ class Widget
16
15
  },
17
16
  color: {
18
17
  type: 'string', index: 'not_analyzed'
18
+ },
19
+ warehouse_id: {
20
+ type: 'string', index: 'not_analyzed'
19
21
  }
20
22
  )
21
-
22
- class << self
23
- def find(ids)
24
- ids.map { |id| new(id: id, color: 'red') }
25
- end
26
23
 
24
+ class << self
27
25
  def anon(&block)
28
26
  Class.new(self) do
29
27
  def self.name
@@ -33,16 +31,9 @@ class Widget
33
31
  instance_eval(&block)
34
32
  end
35
33
  end
36
-
37
- def base_class
38
- self
39
- end
40
34
  end
41
35
 
42
- attr_accessor :id, :name, :color
43
- def initialize(attributes = {})
44
- attributes.each do |key, val|
45
- send("#{key}=", val)
46
- end
36
+ def warehouse=(other)
37
+ self.warehouse_id = other.id
47
38
  end
48
39
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elastic_record
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 0.9.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-10-12 00:00:00.000000000 Z
12
+ date: 2012-10-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: arelastic
@@ -62,6 +62,7 @@ files:
62
62
  - lib/elastic_record/config.rb
63
63
  - lib/elastic_record/connection.rb
64
64
  - lib/elastic_record/index.rb
65
+ - lib/elastic_record/index/deferred.rb
65
66
  - lib/elastic_record/index/documents.rb
66
67
  - lib/elastic_record/index/manage.rb
67
68
  - lib/elastic_record/index/mapping.rb
@@ -79,6 +80,12 @@ files:
79
80
  - lib/elastic_record/relation/none.rb
80
81
  - lib/elastic_record/relation/search_methods.rb
81
82
  - lib/elastic_record/relation/value_methods.rb
83
+ - lib/elastic_record/searches_many.rb
84
+ - lib/elastic_record/searches_many/association.rb
85
+ - lib/elastic_record/searches_many/autosave.rb
86
+ - lib/elastic_record/searches_many/builder.rb
87
+ - lib/elastic_record/searches_many/collection_proxy.rb
88
+ - lib/elastic_record/searches_many/reflection.rb
82
89
  - lib/elastic_record/searching.rb
83
90
  - lib/elastic_record/tasks/index.rake
84
91
  - test/elastic_record/callbacks_test.rb
@@ -99,10 +106,16 @@ files:
99
106
  - test/elastic_record/relation/none_test.rb
100
107
  - test/elastic_record/relation/search_methods_test.rb
101
108
  - test/elastic_record/relation_test.rb
109
+ - test/elastic_record/searches_many/autosave_test.rb
110
+ - test/elastic_record/searches_many/collection_proxy_test.rb
111
+ - test/elastic_record/searches_many/reflection_test.rb
112
+ - test/elastic_record/searches_many_test.rb
102
113
  - test/elastic_record/searching_test.rb
103
114
  - test/helper.rb
104
115
  - test/support/connect.rb
105
- - test/support/widget.rb
116
+ - test/support/models/test_model.rb
117
+ - test/support/models/warehouse.rb
118
+ - test/support/models/widget.rb
106
119
  homepage: http://github.com/matthuhiggins/elastic_record
107
120
  licenses:
108
121
  - MIT