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.
- checksums.yaml +7 -0
- data/.rspec +1 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +146 -0
- data/Rakefile +4 -0
- data/containers/Dockerfile.elasticsearch +7 -0
- data/containers/Dockerfile.opensearch +19 -0
- data/docker-compose.yml +52 -0
- data/lib/active_model/type/array.rb +13 -0
- data/lib/active_model/type/hash.rb +15 -0
- data/lib/rails/instrumentation/publishers.rb +29 -0
- data/lib/rails/instrumentation/railtie.rb +29 -0
- data/lib/stretchy/associations/associated_validator.rb +17 -0
- data/lib/stretchy/associations/elastic_relation.rb +38 -0
- data/lib/stretchy/associations.rb +161 -0
- data/lib/stretchy/common.rb +33 -0
- data/lib/stretchy/delegation/delegate_cache.rb +131 -0
- data/lib/stretchy/delegation/gateway_delegation.rb +43 -0
- data/lib/stretchy/indexing/bulk.rb +48 -0
- data/lib/stretchy/model/callbacks.rb +31 -0
- data/lib/stretchy/model/serialization.rb +20 -0
- data/lib/stretchy/null_relation.rb +53 -0
- data/lib/stretchy/persistence.rb +43 -0
- data/lib/stretchy/querying.rb +20 -0
- data/lib/stretchy/record.rb +57 -0
- data/lib/stretchy/refreshable.rb +15 -0
- data/lib/stretchy/relation.rb +169 -0
- data/lib/stretchy/relations/finder_methods.rb +39 -0
- data/lib/stretchy/relations/merger.rb +179 -0
- data/lib/stretchy/relations/query_builder.rb +265 -0
- data/lib/stretchy/relations/query_methods.rb +578 -0
- data/lib/stretchy/relations/search_option_methods.rb +34 -0
- data/lib/stretchy/relations/spawn_methods.rb +60 -0
- data/lib/stretchy/repository.rb +10 -0
- data/lib/stretchy/scoping/default.rb +134 -0
- data/lib/stretchy/scoping/named.rb +68 -0
- data/lib/stretchy/scoping/scope_registry.rb +34 -0
- data/lib/stretchy/scoping.rb +28 -0
- data/lib/stretchy/shared_scopes.rb +34 -0
- data/lib/stretchy/utils.rb +69 -0
- data/lib/stretchy/version.rb +5 -0
- data/lib/stretchy.rb +38 -0
- data/sig/stretchy.rbs +4 -0
- data/stretchy.logo.png +0 -0
- 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
|