stretchy-model 0.1.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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +1 -0
  3. data/.yardopts +1 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +84 -0
  6. data/Gemfile +10 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +146 -0
  9. data/Rakefile +4 -0
  10. data/containers/Dockerfile.elasticsearch +7 -0
  11. data/containers/Dockerfile.opensearch +19 -0
  12. data/docker-compose.yml +52 -0
  13. data/lib/active_model/type/array.rb +13 -0
  14. data/lib/active_model/type/hash.rb +15 -0
  15. data/lib/rails/instrumentation/publishers.rb +29 -0
  16. data/lib/rails/instrumentation/railtie.rb +29 -0
  17. data/lib/stretchy/associations/associated_validator.rb +17 -0
  18. data/lib/stretchy/associations/elastic_relation.rb +38 -0
  19. data/lib/stretchy/associations.rb +161 -0
  20. data/lib/stretchy/common.rb +33 -0
  21. data/lib/stretchy/delegation/delegate_cache.rb +131 -0
  22. data/lib/stretchy/delegation/gateway_delegation.rb +43 -0
  23. data/lib/stretchy/indexing/bulk.rb +48 -0
  24. data/lib/stretchy/model/callbacks.rb +31 -0
  25. data/lib/stretchy/model/serialization.rb +20 -0
  26. data/lib/stretchy/null_relation.rb +53 -0
  27. data/lib/stretchy/persistence.rb +43 -0
  28. data/lib/stretchy/querying.rb +20 -0
  29. data/lib/stretchy/record.rb +57 -0
  30. data/lib/stretchy/refreshable.rb +15 -0
  31. data/lib/stretchy/relation.rb +169 -0
  32. data/lib/stretchy/relations/finder_methods.rb +39 -0
  33. data/lib/stretchy/relations/merger.rb +179 -0
  34. data/lib/stretchy/relations/query_builder.rb +265 -0
  35. data/lib/stretchy/relations/query_methods.rb +578 -0
  36. data/lib/stretchy/relations/search_option_methods.rb +34 -0
  37. data/lib/stretchy/relations/spawn_methods.rb +60 -0
  38. data/lib/stretchy/repository.rb +10 -0
  39. data/lib/stretchy/scoping/default.rb +134 -0
  40. data/lib/stretchy/scoping/named.rb +68 -0
  41. data/lib/stretchy/scoping/scope_registry.rb +34 -0
  42. data/lib/stretchy/scoping.rb +28 -0
  43. data/lib/stretchy/shared_scopes.rb +34 -0
  44. data/lib/stretchy/utils.rb +69 -0
  45. data/lib/stretchy/version.rb +5 -0
  46. data/lib/stretchy.rb +38 -0
  47. data/sig/stretchy.rbs +4 -0
  48. data/stretchy.logo.png +0 -0
  49. metadata +247 -0
@@ -0,0 +1,39 @@
1
+ module Stretchy
2
+ module Relations
3
+
4
+ module FinderMethods
5
+
6
+ def first
7
+ return results.first if @loaded
8
+ spawn.first!.results.first
9
+ end
10
+
11
+ def first!
12
+ spawn.sort(Hash[default_sort_key, :asc]).spawn.size(1)
13
+ self
14
+ end
15
+
16
+ def last
17
+ return results.last if @loaded
18
+ spawn.last!.results.first
19
+ end
20
+
21
+ def last!
22
+ spawn.sort(Hash[default_sort_key, :desc]).spawn.size(1)
23
+ self
24
+ end
25
+
26
+ def count
27
+ return results.count if @loaded
28
+ spawn.count!
29
+ end
30
+
31
+ def count!
32
+ @values[:count] = true
33
+ @values.delete(:size)
34
+ spawn.results
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,179 @@
1
+ require 'active_support/core_ext/hash/keys'
2
+ require "set"
3
+
4
+ module Stretchy
5
+ module Relations
6
+ class Relation
7
+ class HashMerger # :nodoc:
8
+ attr_reader :relation, :hash
9
+
10
+ def initialize(relation, hash)
11
+ hash.assert_valid_keys(*Relation::VALUE_METHODS)
12
+
13
+ @relation = relation
14
+ @hash = hash
15
+ end
16
+
17
+ def merge
18
+ Merger.new(relation, other).merge
19
+ end
20
+
21
+ # Applying values to a relation has some side effects. E.g.
22
+ # interpolation might take place for where values. So we should
23
+ # build a relation to merge in rather than directly merging
24
+ # the values.
25
+ def other
26
+ other = Relation.create(relation.klass)
27
+ hash.each { |k, v|
28
+ if k == :joins
29
+ if Hash === v
30
+ other.joins!(v)
31
+ else
32
+ other.joins!(*v)
33
+ end
34
+ elsif k == :select
35
+ other._select!(v)
36
+ else
37
+ other.send("#{k}!", v)
38
+ end
39
+ }
40
+ other
41
+ end
42
+ end
43
+
44
+ class Merger # :nodoc:
45
+ attr_reader :relation, :values, :other
46
+
47
+ def initialize(relation, other)
48
+ @relation = relation
49
+ @values = other.values
50
+ @other = other
51
+ end
52
+
53
+ NORMAL_VALUES = [:where, :first, :last, :filter]
54
+
55
+ def normal_values
56
+ NORMAL_VALUES
57
+ end
58
+
59
+ def merge
60
+ normal_values.each do |name|
61
+ value = values[name]
62
+ # The unless clause is here mostly for performance reasons (since the `send` call might be moderately
63
+ # expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that
64
+ # `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values
65
+ # don't fall through the cracks.
66
+
67
+ unless value.nil? || (value.blank? && false != value)
68
+ if name == :select
69
+ relation._select!(*value)
70
+ elsif name == :filter
71
+ values.each do |v|
72
+ relation.send("#{name}!", v.first, v.last)
73
+ end
74
+ else
75
+ relation.send("#{name}!", *value)
76
+ end
77
+ end
78
+ end
79
+
80
+ merge_multi_values
81
+ merge_single_values
82
+ #merge_joins
83
+
84
+ relation
85
+ end
86
+
87
+ private
88
+
89
+ def merge_joins
90
+ return if values[:joins].blank?
91
+
92
+ if other.klass == relation.klass
93
+ relation.joins!(*values[:joins])
94
+ else
95
+ joins_dependency, rest = values[:joins].partition do |join|
96
+ case join
97
+ when Hash, Symbol, Array
98
+ true
99
+ else
100
+ false
101
+ end
102
+ end
103
+
104
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(other.klass,
105
+ joins_dependency,
106
+ [])
107
+ relation.joins! rest
108
+
109
+ @relation = relation.joins join_dependency
110
+ end
111
+ end
112
+
113
+ def merge_multi_values
114
+ lhs_wheres = relation.where_values
115
+ rhs_wheres = values[:where] || []
116
+
117
+ lhs_filters = relation.filter_values
118
+ rhs_filters = values[:filter] || []
119
+
120
+ removed, kept = partition_overwrites(lhs_wheres, rhs_wheres)
121
+
122
+ where_values = kept + rhs_wheres
123
+
124
+ filters_removed, filters_kept = partition_overwrites(lhs_wheres, rhs_wheres)
125
+ filter_values = rhs_filters
126
+
127
+
128
+ relation.where_values = where_values.empty? ? nil : where_values
129
+ relation.filter_values = filter_values.empty? ? nil : filter_values
130
+
131
+ if values[:reordering]
132
+ # override any order specified in the original relation
133
+ relation.reorder! values[:order]
134
+ elsif values[:order]
135
+ # merge in order_values from relation
136
+ relation.order! values[:order]
137
+ end
138
+
139
+ relation.extend(*values[:extending]) unless values[:extending].blank?
140
+ end
141
+
142
+ def merge_single_values
143
+ #relation.from_value = values[:from] unless relation.from_value
144
+ #relation.lock_value = values[:lock] unless relation.lock_value
145
+
146
+ unless values[:create_with].blank?
147
+ relation.create_with_value = (relation.create_with_value || {}).merge(values[:create_with])
148
+ end
149
+ end
150
+
151
+ def filter_binds(lhs_binds, removed_wheres)
152
+ return lhs_binds if removed_wheres.empty?
153
+
154
+ set = Set.new removed_wheres.map { |x| x.left.name.to_s }
155
+ lhs_binds.dup.delete_if { |col,_| set.include? col.name }
156
+ end
157
+
158
+ # Remove equalities from the existing relation with a LHS which is
159
+ # present in the relation being merged in.
160
+ # returns [things_to_remove, things_to_keep]
161
+ def partition_overwrites(lhs_wheres, rhs_wheres)
162
+ if lhs_wheres.empty? || rhs_wheres.empty?
163
+ return [[], lhs_wheres]
164
+ end
165
+
166
+ nodes = rhs_wheres.find_all do |w|
167
+ w.respond_to?(:operator) && w.operator == :==
168
+ end
169
+ seen = Set.new(nodes) { |node| node.left }
170
+
171
+ lhs_wheres.partition do |w|
172
+ w.respond_to?(:operator) && w.operator == :== && seen.include?(w.left)
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+
@@ -0,0 +1,265 @@
1
+ module Stretchy
2
+ module Relations
3
+ class QueryBuilder
4
+
5
+ attr_reader :structure, :values
6
+
7
+ def initialize(values)
8
+ @structure = Jbuilder.new ignore_nil: true
9
+ @values = values
10
+ end
11
+
12
+ def aggregations
13
+ values[:aggregation]
14
+ end
15
+
16
+ def filters
17
+ values[:filter]
18
+ end
19
+
20
+ def or_filters
21
+ values[:or_filter]
22
+ end
23
+
24
+ def query
25
+ @query ||= compact_where(values[:where])
26
+ end
27
+
28
+ def query_strings
29
+ @query_string ||= compact_where(values[:query_string], bool: false)
30
+ end
31
+
32
+ def must_nots
33
+ @must_nots ||= compact_where(values[:must_not])
34
+ end
35
+
36
+ def shoulds
37
+ @shoulds ||= compact_where(values[:should])
38
+ end
39
+
40
+ def fields
41
+ values[:field]
42
+ end
43
+
44
+ def source
45
+ values[:source]
46
+ end
47
+
48
+ def highlights
49
+ values[:highlight]
50
+ end
51
+
52
+ def size
53
+ values[:size]
54
+ end
55
+
56
+ def sort
57
+ values[:order]
58
+ end
59
+
60
+ def query_filters
61
+ values[:filter]
62
+ end
63
+
64
+ def search_options
65
+ build_search_options
66
+ end
67
+
68
+ def query_string_options
69
+ @query_string_options || {}
70
+ end
71
+
72
+ def count?
73
+ values[:count]
74
+ end
75
+
76
+ def to_elastic
77
+ @structure = Jbuilder.new ignore_nil: true
78
+ build_query
79
+ build_sort unless sort.blank?
80
+ build_highlights unless highlights.blank?
81
+ build_fields unless fields.blank?
82
+ build_source unless source.blank?
83
+ build_aggregations unless aggregations.blank?
84
+ structure.attributes!.with_indifferent_access
85
+ end
86
+
87
+ private
88
+
89
+ def missing_bool_query?
90
+ query.nil? && must_nots.nil? && shoulds.nil?
91
+ end
92
+
93
+ def missing_query_string?
94
+ query_strings.nil?
95
+ end
96
+
97
+ def missing_query_filter?
98
+ query_filters.nil? && or_filters.nil?
99
+ end
100
+
101
+ def build_query
102
+ return if missing_bool_query? && missing_query_string? && missing_query_filter?
103
+ structure.query do
104
+ structure.bool do
105
+ structure.must query unless missing_bool_query?
106
+ structure.must_not must_nots unless must_nots.nil?
107
+ structure.set! :should, shoulds unless shoulds.nil?
108
+
109
+ build_filtered_query if query_filters || or_filters
110
+
111
+ end unless missing_bool_query? && missing_query_filter?
112
+
113
+
114
+
115
+ structure.query_string do
116
+ structure.extract! query_string_options, *query_string_options.keys
117
+ structure.query query_strings
118
+ end unless query_strings.nil?
119
+ end.with_indifferent_access
120
+ end
121
+
122
+ def build_filtered_query
123
+ structure.filter do
124
+ structure.or do
125
+ or_filters.each do |f|
126
+ structure.child! do
127
+ structure.set! f[:name], extract_filters(f[:name], f[:args])
128
+ end
129
+ end
130
+ end unless or_filters.blank?
131
+
132
+ query_filters.each do |f|
133
+ structure.child! do
134
+ structure.set! f[:name], extract_filters(f[:name], f[:args])
135
+ end
136
+ end unless query_filters.blank?
137
+ end
138
+ end
139
+
140
+ def build_source
141
+ if [true,false].include? source.first
142
+ structure._source source.first
143
+ else
144
+ structure._source do
145
+ structure.includes source.first.delete(:includes) if source.first.has_key? :includes
146
+ structure.excludes source.first.delete(:excludes) if source.first.has_key? :excludes
147
+ end
148
+ end
149
+ end
150
+
151
+ def build_sort
152
+ structure.sort sort.flatten #.inject(Hash.new) { |h,v| h.merge(v) }
153
+ end
154
+
155
+ def build_highlights
156
+ structure.highlight do
157
+ structure.fields do
158
+ highlights.each do |highlight|
159
+ structure.set! highlight, extract_highlighter(highlight)
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ def build_aggregations
166
+ structure.aggregations do
167
+ aggregations.each do |agg|
168
+ structure.set! agg[:name], aggregation(agg[:name], agg[:args])
169
+ end
170
+ end
171
+ end
172
+
173
+ def build_fields
174
+ structure.fields do
175
+ structure.array! fields.flatten
176
+ end
177
+ end
178
+
179
+ def build_search_options
180
+ values[:search_option] ||= []
181
+
182
+ opts = extra_search_options
183
+ (values[:search_option] + [opts]).compact.inject(Hash.new) { |h,k,v| h.merge(k) }
184
+ end
185
+
186
+ def extra_search_options
187
+ [:size].inject(Hash.new) { |h,k| h[k] = self.send(k) unless self.send(k).nil?; h}
188
+ end
189
+
190
+ def compact_where(q, opts = {bool:true})
191
+ return if q.nil?
192
+ if opts.delete(:bool)
193
+ as_must(q)
194
+ else
195
+ as_query_string(q.flatten)
196
+ end
197
+ end
198
+
199
+ def as_must(q)
200
+ _must = []
201
+ q.each do |arg|
202
+ arg.each_pair { |k,v| _must << (v.is_a?(Array) ? {terms: Hash[k,v]} : {term: Hash[k,v]}) } if arg.class == Hash
203
+ _must << {term: Hash[[arg.split(/:/).collect(&:strip)]]} if arg.class == String
204
+ _must << arg.first if arg.class == Array
205
+ end
206
+ _must.length == 1 ? _must.first : _must
207
+ end
208
+
209
+ def as_query_string(q)
210
+ _and = []
211
+
212
+ @query_string_options = q.pop if q.length > 1
213
+
214
+ q.each do |arg|
215
+ arg.each_pair { |k,v| _and << "(#{k}:#{v})" } if arg.class == Hash
216
+ _and << "(#{arg})" if arg.class == String
217
+ end
218
+ _and.join(" AND ")
219
+ end
220
+
221
+
222
+
223
+ def extract_highlighter(highlighter)
224
+ Jbuilder.new do |highlight|
225
+ highlight.extract! highlighter
226
+ end
227
+ end
228
+
229
+ def extract_filters(name,opts = {})
230
+ Jbuilder.new do |filter|
231
+ case
232
+ when opts.is_a?(Hash)
233
+ filter.extract! opts, *opts.keys
234
+ when opts.is_a?(Array)
235
+ extract_filter_arguments_from_array(filter, opts)
236
+ else
237
+ raise "#filter only accepts Hash or Array"
238
+ end
239
+ end
240
+ end
241
+
242
+ def aggregation(name, opts = {})
243
+ Jbuilder.new do |agg|
244
+ case
245
+ when opts.is_a?(Hash)
246
+ agg.extract! opts, *opts.keys
247
+ when opts.is_a?(Array)
248
+ extract_filter_arguments_from_array(agg, opts)
249
+ else
250
+ raise "#aggregation only accepts Hash or Array"
251
+ end
252
+ end
253
+ end
254
+
255
+ def extract_filter_arguments_from_array(element, opts)
256
+ opts.each do |opt|
257
+ element.child! do
258
+ element.extract! opt , *opt.keys
259
+ end
260
+ end
261
+ end
262
+
263
+ end
264
+ end
265
+ end