searchkick 2.3.2 → 5.2.1
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 +5 -5
- data/CHANGELOG.md +377 -84
- data/LICENSE.txt +1 -1
- data/README.md +859 -602
- data/lib/searchkick/bulk_reindex_job.rb +13 -9
- data/lib/searchkick/controller_runtime.rb +40 -0
- data/lib/searchkick/hash_wrapper.rb +12 -0
- data/lib/searchkick/index.rb +281 -356
- data/lib/searchkick/index_cache.rb +30 -0
- data/lib/searchkick/index_options.rb +487 -281
- data/lib/searchkick/indexer.rb +15 -8
- data/lib/searchkick/log_subscriber.rb +57 -0
- data/lib/searchkick/middleware.rb +9 -2
- data/lib/searchkick/model.rb +72 -118
- data/lib/searchkick/multi_search.rb +9 -10
- data/lib/searchkick/process_batch_job.rb +12 -15
- data/lib/searchkick/process_queue_job.rb +22 -13
- data/lib/searchkick/query.rb +458 -217
- data/lib/searchkick/railtie.rb +7 -0
- data/lib/searchkick/record_data.rb +128 -0
- data/lib/searchkick/record_indexer.rb +164 -0
- data/lib/searchkick/reindex_queue.rb +51 -9
- data/lib/searchkick/reindex_v2_job.rb +10 -32
- data/lib/searchkick/relation.rb +247 -0
- data/lib/searchkick/relation_indexer.rb +155 -0
- data/lib/searchkick/results.rb +201 -82
- data/lib/searchkick/version.rb +1 -1
- data/lib/searchkick/where.rb +11 -0
- data/lib/searchkick.rb +269 -97
- data/lib/tasks/searchkick.rake +37 -0
- metadata +24 -178
- data/.gitignore +0 -22
- data/.travis.yml +0 -39
- data/Gemfile +0 -16
- data/Rakefile +0 -20
- data/benchmark/Gemfile +0 -23
- data/benchmark/benchmark.rb +0 -97
- data/lib/searchkick/logging.rb +0 -242
- data/lib/searchkick/tasks.rb +0 -33
- data/searchkick.gemspec +0 -28
- data/test/aggs_test.rb +0 -197
- data/test/autocomplete_test.rb +0 -75
- data/test/boost_test.rb +0 -202
- data/test/callbacks_test.rb +0 -59
- data/test/ci/before_install.sh +0 -17
- data/test/errors_test.rb +0 -19
- data/test/gemfiles/activerecord31.gemfile +0 -7
- data/test/gemfiles/activerecord32.gemfile +0 -7
- data/test/gemfiles/activerecord40.gemfile +0 -8
- data/test/gemfiles/activerecord41.gemfile +0 -8
- data/test/gemfiles/activerecord42.gemfile +0 -7
- data/test/gemfiles/activerecord50.gemfile +0 -7
- data/test/gemfiles/apartment.gemfile +0 -8
- data/test/gemfiles/cequel.gemfile +0 -8
- data/test/gemfiles/mongoid2.gemfile +0 -7
- data/test/gemfiles/mongoid3.gemfile +0 -6
- data/test/gemfiles/mongoid4.gemfile +0 -7
- data/test/gemfiles/mongoid5.gemfile +0 -7
- data/test/gemfiles/mongoid6.gemfile +0 -12
- data/test/gemfiles/nobrainer.gemfile +0 -8
- data/test/gemfiles/parallel_tests.gemfile +0 -8
- data/test/geo_shape_test.rb +0 -175
- data/test/highlight_test.rb +0 -78
- data/test/index_test.rb +0 -166
- data/test/inheritance_test.rb +0 -83
- data/test/marshal_test.rb +0 -8
- data/test/match_test.rb +0 -276
- data/test/misspellings_test.rb +0 -56
- data/test/model_test.rb +0 -42
- data/test/multi_search_test.rb +0 -36
- data/test/multi_tenancy_test.rb +0 -22
- data/test/order_test.rb +0 -46
- data/test/pagination_test.rb +0 -70
- data/test/partial_reindex_test.rb +0 -58
- data/test/query_test.rb +0 -35
- data/test/records_test.rb +0 -10
- data/test/reindex_test.rb +0 -64
- data/test/reindex_v2_job_test.rb +0 -32
- data/test/routing_test.rb +0 -23
- data/test/should_index_test.rb +0 -32
- data/test/similar_test.rb +0 -28
- data/test/sql_test.rb +0 -214
- data/test/suggest_test.rb +0 -95
- data/test/support/kaminari.yml +0 -21
- data/test/synonyms_test.rb +0 -67
- data/test/test_helper.rb +0 -567
- data/test/where_test.rb +0 -223
@@ -0,0 +1,128 @@
|
|
1
|
+
module Searchkick
|
2
|
+
class RecordData
|
3
|
+
TYPE_KEYS = ["type", :type]
|
4
|
+
|
5
|
+
attr_reader :index, :record
|
6
|
+
|
7
|
+
def initialize(index, record)
|
8
|
+
@index = index
|
9
|
+
@record = record
|
10
|
+
end
|
11
|
+
|
12
|
+
def index_data
|
13
|
+
data = record_data
|
14
|
+
data[:data] = search_data
|
15
|
+
{index: data}
|
16
|
+
end
|
17
|
+
|
18
|
+
def update_data(method_name)
|
19
|
+
data = record_data
|
20
|
+
data[:data] = {doc: search_data(method_name)}
|
21
|
+
{update: data}
|
22
|
+
end
|
23
|
+
|
24
|
+
def delete_data
|
25
|
+
{delete: record_data}
|
26
|
+
end
|
27
|
+
|
28
|
+
# custom id can be useful for load: false
|
29
|
+
def search_id
|
30
|
+
id = record.respond_to?(:search_document_id) ? record.search_document_id : record.id
|
31
|
+
id.is_a?(Numeric) ? id : id.to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
def document_type(ignore_type = false)
|
35
|
+
index.klass_document_type(record.class, ignore_type)
|
36
|
+
end
|
37
|
+
|
38
|
+
def record_data
|
39
|
+
data = {
|
40
|
+
_index: index.name,
|
41
|
+
_id: search_id
|
42
|
+
}
|
43
|
+
data[:routing] = record.search_routing if record.respond_to?(:search_routing)
|
44
|
+
data
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def search_data(method_name = nil)
|
50
|
+
partial_reindex = !method_name.nil?
|
51
|
+
|
52
|
+
source = record.send(method_name || :search_data)
|
53
|
+
|
54
|
+
# conversions
|
55
|
+
index.conversions_fields.each do |conversions_field|
|
56
|
+
if source[conversions_field]
|
57
|
+
source[conversions_field] = source[conversions_field].map { |k, v| {query: k, count: v} }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# hack to prevent generator field doesn't exist error
|
62
|
+
if !partial_reindex
|
63
|
+
index.suggest_fields.each do |field|
|
64
|
+
if !source.key?(field) && !source.key?(field.to_sym)
|
65
|
+
source[field] = nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# locations
|
71
|
+
index.locations_fields.each do |field|
|
72
|
+
if source[field]
|
73
|
+
if !source[field].is_a?(Hash) && (source[field].first.is_a?(Array) || source[field].first.is_a?(Hash))
|
74
|
+
# multiple locations
|
75
|
+
source[field] = source[field].map { |a| location_value(a) }
|
76
|
+
else
|
77
|
+
source[field] = location_value(source[field])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
if index.options[:inheritance]
|
83
|
+
if !TYPE_KEYS.any? { |tk| source.key?(tk) }
|
84
|
+
source[:type] = document_type(true)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
cast_big_decimal(source)
|
89
|
+
|
90
|
+
source
|
91
|
+
end
|
92
|
+
|
93
|
+
def location_value(value)
|
94
|
+
if value.is_a?(Array)
|
95
|
+
value.map(&:to_f).reverse
|
96
|
+
elsif value.is_a?(Hash)
|
97
|
+
{lat: value[:lat].to_f, lon: value[:lon].to_f}
|
98
|
+
else
|
99
|
+
value
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# change all BigDecimal values to floats due to
|
104
|
+
# https://github.com/rails/rails/issues/6033
|
105
|
+
# possible loss of precision :/
|
106
|
+
def cast_big_decimal(obj)
|
107
|
+
case obj
|
108
|
+
when BigDecimal
|
109
|
+
obj.to_f
|
110
|
+
when Hash
|
111
|
+
obj.each do |k, v|
|
112
|
+
# performance
|
113
|
+
if v.is_a?(BigDecimal)
|
114
|
+
obj[k] = v.to_f
|
115
|
+
elsif v.is_a?(Enumerable)
|
116
|
+
obj[k] = cast_big_decimal(v)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
when Enumerable
|
120
|
+
obj.map do |v|
|
121
|
+
cast_big_decimal(v)
|
122
|
+
end
|
123
|
+
else
|
124
|
+
obj
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
module Searchkick
|
2
|
+
class RecordIndexer
|
3
|
+
attr_reader :index
|
4
|
+
|
5
|
+
def initialize(index)
|
6
|
+
@index = index
|
7
|
+
end
|
8
|
+
|
9
|
+
def reindex(records, mode:, method_name:, full: false, single: false)
|
10
|
+
# prevents exists? check if records is a relation
|
11
|
+
records = records.to_a
|
12
|
+
return if records.empty?
|
13
|
+
|
14
|
+
case mode
|
15
|
+
when :async
|
16
|
+
unless defined?(ActiveJob)
|
17
|
+
raise Error, "Active Job not found"
|
18
|
+
end
|
19
|
+
|
20
|
+
# we could likely combine ReindexV2Job, BulkReindexJob, and ProcessBatchJob
|
21
|
+
# but keep them separate for now
|
22
|
+
if single
|
23
|
+
record = records.first
|
24
|
+
|
25
|
+
# always pass routing in case record is deleted
|
26
|
+
# before the async job runs
|
27
|
+
if record.respond_to?(:search_routing)
|
28
|
+
routing = record.search_routing
|
29
|
+
end
|
30
|
+
|
31
|
+
Searchkick::ReindexV2Job.perform_later(
|
32
|
+
record.class.name,
|
33
|
+
record.id.to_s,
|
34
|
+
method_name ? method_name.to_s : nil,
|
35
|
+
routing: routing,
|
36
|
+
index_name: index.name
|
37
|
+
)
|
38
|
+
else
|
39
|
+
Searchkick::BulkReindexJob.perform_later(
|
40
|
+
class_name: records.first.class.searchkick_options[:class_name],
|
41
|
+
record_ids: records.map { |r| r.id.to_s },
|
42
|
+
index_name: index.name,
|
43
|
+
method_name: method_name ? method_name.to_s : nil
|
44
|
+
)
|
45
|
+
end
|
46
|
+
when :queue
|
47
|
+
if method_name
|
48
|
+
raise Error, "Partial reindex not supported with queue option"
|
49
|
+
end
|
50
|
+
|
51
|
+
index.reindex_queue.push_records(records)
|
52
|
+
when true, :inline
|
53
|
+
index_records, other_records = records.partition { |r| index_record?(r) }
|
54
|
+
import_inline(index_records, !full ? other_records : [], method_name: method_name, single: single)
|
55
|
+
else
|
56
|
+
raise ArgumentError, "Invalid value for mode"
|
57
|
+
end
|
58
|
+
|
59
|
+
# return true like model and relation reindex for now
|
60
|
+
true
|
61
|
+
end
|
62
|
+
|
63
|
+
def reindex_items(klass, items, method_name:, single: false)
|
64
|
+
routing = items.to_h { |r| [r[:id], r[:routing]] }
|
65
|
+
record_ids = routing.keys
|
66
|
+
|
67
|
+
relation = Searchkick.load_records(klass, record_ids)
|
68
|
+
# call search_import even for single records for nested associations
|
69
|
+
relation = relation.search_import if relation.respond_to?(:search_import)
|
70
|
+
records = relation.select(&:should_index?)
|
71
|
+
|
72
|
+
# determine which records to delete
|
73
|
+
delete_ids = record_ids - records.map { |r| r.id.to_s }
|
74
|
+
delete_records =
|
75
|
+
delete_ids.map do |id|
|
76
|
+
construct_record(klass, id, routing[id])
|
77
|
+
end
|
78
|
+
|
79
|
+
import_inline(records, delete_records, method_name: method_name, single: single)
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def index_record?(record)
|
85
|
+
record.persisted? && !record.destroyed? && record.should_index?
|
86
|
+
end
|
87
|
+
|
88
|
+
# import in single request with retries
|
89
|
+
def import_inline(index_records, delete_records, method_name:, single:)
|
90
|
+
return if index_records.empty? && delete_records.empty?
|
91
|
+
|
92
|
+
maybe_bulk(index_records, delete_records, method_name, single) do
|
93
|
+
if index_records.any?
|
94
|
+
if method_name
|
95
|
+
index.bulk_update(index_records, method_name)
|
96
|
+
else
|
97
|
+
index.bulk_index(index_records)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
if delete_records.any?
|
102
|
+
index.bulk_delete(delete_records)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def maybe_bulk(index_records, delete_records, method_name, single)
|
108
|
+
if Searchkick.callbacks_value == :bulk
|
109
|
+
yield
|
110
|
+
else
|
111
|
+
# set action and data
|
112
|
+
action =
|
113
|
+
if single && index_records.empty?
|
114
|
+
"Remove"
|
115
|
+
elsif method_name
|
116
|
+
"Update"
|
117
|
+
else
|
118
|
+
single ? "Store" : "Import"
|
119
|
+
end
|
120
|
+
record = index_records.first || delete_records.first
|
121
|
+
name = record.class.searchkick_klass.name
|
122
|
+
message = lambda do |event|
|
123
|
+
event[:name] = "#{name} #{action}"
|
124
|
+
if single
|
125
|
+
event[:id] = index.search_id(record)
|
126
|
+
else
|
127
|
+
event[:count] = index_records.size + delete_records.size
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
with_retries do
|
132
|
+
Searchkick.callbacks(:bulk, message: message) do
|
133
|
+
yield
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def construct_record(klass, id, routing)
|
140
|
+
record = klass.new
|
141
|
+
record.id = id
|
142
|
+
if routing
|
143
|
+
record.define_singleton_method(:search_routing) do
|
144
|
+
routing
|
145
|
+
end
|
146
|
+
end
|
147
|
+
record
|
148
|
+
end
|
149
|
+
|
150
|
+
def with_retries
|
151
|
+
retries = 0
|
152
|
+
|
153
|
+
begin
|
154
|
+
yield
|
155
|
+
rescue Faraday::ClientError => e
|
156
|
+
if retries < 1
|
157
|
+
retries += 1
|
158
|
+
retry
|
159
|
+
end
|
160
|
+
raise e
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -5,28 +5,53 @@ module Searchkick
|
|
5
5
|
def initialize(name)
|
6
6
|
@name = name
|
7
7
|
|
8
|
-
raise
|
8
|
+
raise Error, "Searchkick.redis not set" unless Searchkick.redis
|
9
9
|
end
|
10
10
|
|
11
|
-
|
12
|
-
|
11
|
+
# supports single and multiple ids
|
12
|
+
def push(record_ids)
|
13
|
+
Searchkick.with_redis { |r| r.call("LPUSH", redis_key, record_ids) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def push_records(records)
|
17
|
+
record_ids =
|
18
|
+
records.map do |record|
|
19
|
+
# always pass routing in case record is deleted
|
20
|
+
# before the queue job runs
|
21
|
+
if record.respond_to?(:search_routing)
|
22
|
+
routing = record.search_routing
|
23
|
+
end
|
24
|
+
|
25
|
+
# escape pipe with double pipe
|
26
|
+
value = escape(record.id.to_s)
|
27
|
+
value = "#{value}|#{escape(routing)}" if routing
|
28
|
+
value
|
29
|
+
end
|
30
|
+
|
31
|
+
push(record_ids)
|
13
32
|
end
|
14
33
|
|
15
34
|
# TODO use reliable queuing
|
16
35
|
def reserve(limit: 1000)
|
17
|
-
|
18
|
-
|
19
|
-
|
36
|
+
if supports_rpop_with_count?
|
37
|
+
Searchkick.with_redis { |r| r.call("RPOP", redis_key, limit) }.to_a
|
38
|
+
else
|
39
|
+
record_ids = []
|
40
|
+
Searchkick.with_redis do |r|
|
41
|
+
while record_ids.size < limit && (record_id = r.call("RPOP", redis_key))
|
42
|
+
record_ids << record_id
|
43
|
+
end
|
44
|
+
end
|
45
|
+
record_ids
|
20
46
|
end
|
21
|
-
record_ids.to_a
|
22
47
|
end
|
23
48
|
|
24
49
|
def clear
|
25
|
-
Searchkick.with_redis { |r| r.
|
50
|
+
Searchkick.with_redis { |r| r.call("DEL", redis_key) }
|
26
51
|
end
|
27
52
|
|
28
53
|
def length
|
29
|
-
Searchkick.with_redis { |r| r.
|
54
|
+
Searchkick.with_redis { |r| r.call("LLEN", redis_key) }
|
30
55
|
end
|
31
56
|
|
32
57
|
private
|
@@ -34,5 +59,22 @@ module Searchkick
|
|
34
59
|
def redis_key
|
35
60
|
"searchkick:reindex_queue:#{name}"
|
36
61
|
end
|
62
|
+
|
63
|
+
def supports_rpop_with_count?
|
64
|
+
redis_version >= Gem::Version.new("6.2")
|
65
|
+
end
|
66
|
+
|
67
|
+
def redis_version
|
68
|
+
@redis_version ||=
|
69
|
+
Searchkick.with_redis do |r|
|
70
|
+
info = r.call("INFO")
|
71
|
+
matches = /redis_version:(\S+)/.match(info)
|
72
|
+
Gem::Version.new(matches[1])
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def escape(value)
|
77
|
+
value.gsub("|", "||")
|
78
|
+
end
|
37
79
|
end
|
38
80
|
end
|
@@ -1,39 +1,17 @@
|
|
1
1
|
module Searchkick
|
2
2
|
class ReindexV2Job < ActiveJob::Base
|
3
|
-
RECORD_NOT_FOUND_CLASSES = [
|
4
|
-
"ActiveRecord::RecordNotFound",
|
5
|
-
"Mongoid::Errors::DocumentNotFound",
|
6
|
-
"NoBrainer::Error::DocumentNotFound",
|
7
|
-
"Cequel::Record::RecordNotFound"
|
8
|
-
]
|
9
|
-
|
10
3
|
queue_as { Searchkick.queue_name }
|
11
4
|
|
12
|
-
def perform(
|
13
|
-
model =
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
23
|
-
|
24
|
-
index = model.searchkick_index
|
25
|
-
if !record || !record.should_index?
|
26
|
-
# hacky
|
27
|
-
record ||= model.new
|
28
|
-
record.id = id
|
29
|
-
begin
|
30
|
-
index.remove record
|
31
|
-
rescue Elasticsearch::Transport::Transport::Errors::NotFound
|
32
|
-
# do nothing
|
33
|
-
end
|
34
|
-
else
|
35
|
-
index.store record
|
36
|
-
end
|
5
|
+
def perform(class_name, id, method_name = nil, routing: nil, index_name: nil)
|
6
|
+
model = Searchkick.load_model(class_name, allow_child: true)
|
7
|
+
index = model.searchkick_index(name: index_name)
|
8
|
+
# use should_index? to decide whether to index (not default scope)
|
9
|
+
# just like saving inline
|
10
|
+
# could use Searchkick.scope() in future
|
11
|
+
# but keep for now for backwards compatibility
|
12
|
+
model = model.unscoped if model.respond_to?(:unscoped)
|
13
|
+
items = [{id: id, routing: routing}]
|
14
|
+
RecordIndexer.new(index).reindex_items(model, items, method_name: method_name, single: true)
|
37
15
|
end
|
38
16
|
end
|
39
17
|
end
|
@@ -0,0 +1,247 @@
|
|
1
|
+
module Searchkick
|
2
|
+
class Relation
|
3
|
+
NO_DEFAULT_VALUE = Object.new
|
4
|
+
|
5
|
+
# note: modifying body directly is not supported
|
6
|
+
# and has no impact on query after being executed
|
7
|
+
# TODO freeze body object?
|
8
|
+
delegate :body, :params, to: :query
|
9
|
+
delegate_missing_to :private_execute
|
10
|
+
|
11
|
+
attr_reader :model
|
12
|
+
alias_method :klass, :model
|
13
|
+
|
14
|
+
def initialize(model, term = "*", **options)
|
15
|
+
@model = model
|
16
|
+
@term = term
|
17
|
+
@options = options
|
18
|
+
|
19
|
+
# generate query to validate options
|
20
|
+
query
|
21
|
+
end
|
22
|
+
|
23
|
+
# same as Active Record
|
24
|
+
def inspect
|
25
|
+
entries = results.first(11).map!(&:inspect)
|
26
|
+
entries[10] = "..." if entries.size == 11
|
27
|
+
"#<#{self.class.name} [#{entries.join(', ')}]>"
|
28
|
+
end
|
29
|
+
|
30
|
+
def execute
|
31
|
+
Searchkick.warn("The execute method is no longer needed")
|
32
|
+
load
|
33
|
+
end
|
34
|
+
|
35
|
+
# experimental
|
36
|
+
def limit(value)
|
37
|
+
clone.limit!(value)
|
38
|
+
end
|
39
|
+
|
40
|
+
# experimental
|
41
|
+
def limit!(value)
|
42
|
+
check_loaded
|
43
|
+
@options[:limit] = value
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
# experimental
|
48
|
+
def offset(value = NO_DEFAULT_VALUE)
|
49
|
+
# TODO remove in Searchkick 6
|
50
|
+
if value == NO_DEFAULT_VALUE
|
51
|
+
private_execute.offset
|
52
|
+
else
|
53
|
+
clone.offset!(value)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# experimental
|
58
|
+
def offset!(value)
|
59
|
+
check_loaded
|
60
|
+
@options[:offset] = value
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
# experimental
|
65
|
+
def page(value)
|
66
|
+
clone.page!(value)
|
67
|
+
end
|
68
|
+
|
69
|
+
# experimental
|
70
|
+
def page!(value)
|
71
|
+
check_loaded
|
72
|
+
@options[:page] = value
|
73
|
+
self
|
74
|
+
end
|
75
|
+
|
76
|
+
# experimental
|
77
|
+
def per_page(value = NO_DEFAULT_VALUE)
|
78
|
+
# TODO remove in Searchkick 6
|
79
|
+
if value == NO_DEFAULT_VALUE
|
80
|
+
private_execute.per_page
|
81
|
+
else
|
82
|
+
clone.per_page!(value)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# experimental
|
87
|
+
def per_page!(value)
|
88
|
+
check_loaded
|
89
|
+
@options[:per_page] = value
|
90
|
+
self
|
91
|
+
end
|
92
|
+
|
93
|
+
# experimental
|
94
|
+
def where(value = NO_DEFAULT_VALUE)
|
95
|
+
if value == NO_DEFAULT_VALUE
|
96
|
+
Where.new(self)
|
97
|
+
else
|
98
|
+
clone.where!(value)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# experimental
|
103
|
+
def where!(value)
|
104
|
+
check_loaded
|
105
|
+
if @options[:where]
|
106
|
+
@options[:where] = {_and: [@options[:where], ensure_permitted(value)]}
|
107
|
+
else
|
108
|
+
@options[:where] = ensure_permitted(value)
|
109
|
+
end
|
110
|
+
self
|
111
|
+
end
|
112
|
+
|
113
|
+
# experimental
|
114
|
+
def rewhere(value)
|
115
|
+
clone.rewhere!(value)
|
116
|
+
end
|
117
|
+
|
118
|
+
# experimental
|
119
|
+
def rewhere!(value)
|
120
|
+
check_loaded
|
121
|
+
@options[:where] = ensure_permitted(value)
|
122
|
+
self
|
123
|
+
end
|
124
|
+
|
125
|
+
# experimental
|
126
|
+
def order(*values)
|
127
|
+
clone.order!(*values)
|
128
|
+
end
|
129
|
+
|
130
|
+
# experimental
|
131
|
+
def order!(*values)
|
132
|
+
values = values.first if values.size == 1 && values.first.is_a?(Array)
|
133
|
+
check_loaded
|
134
|
+
(@options[:order] ||= []).concat(values)
|
135
|
+
self
|
136
|
+
end
|
137
|
+
|
138
|
+
# experimental
|
139
|
+
def reorder(*values)
|
140
|
+
clone.reorder!(*values)
|
141
|
+
end
|
142
|
+
|
143
|
+
# experimental
|
144
|
+
def reorder!(*values)
|
145
|
+
check_loaded
|
146
|
+
@options[:order] = values
|
147
|
+
self
|
148
|
+
end
|
149
|
+
|
150
|
+
# experimental
|
151
|
+
def select(*values, &block)
|
152
|
+
if block_given?
|
153
|
+
private_execute.select(*values, &block)
|
154
|
+
else
|
155
|
+
clone.select!(*values)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# experimental
|
160
|
+
def select!(*values)
|
161
|
+
check_loaded
|
162
|
+
(@options[:select] ||= []).concat(values)
|
163
|
+
self
|
164
|
+
end
|
165
|
+
|
166
|
+
# experimental
|
167
|
+
def reselect(*values)
|
168
|
+
clone.reselect!(*values)
|
169
|
+
end
|
170
|
+
|
171
|
+
# experimental
|
172
|
+
def reselect!(*values)
|
173
|
+
check_loaded
|
174
|
+
@options[:select] = values
|
175
|
+
self
|
176
|
+
end
|
177
|
+
|
178
|
+
# experimental
|
179
|
+
def includes(*values)
|
180
|
+
clone.includes!(*values)
|
181
|
+
end
|
182
|
+
|
183
|
+
# experimental
|
184
|
+
def includes!(*values)
|
185
|
+
check_loaded
|
186
|
+
(@options[:includes] ||= []).concat(values)
|
187
|
+
self
|
188
|
+
end
|
189
|
+
|
190
|
+
# experimental
|
191
|
+
def only(*keys)
|
192
|
+
Relation.new(@model, @term, **@options.slice(*keys))
|
193
|
+
end
|
194
|
+
|
195
|
+
# experimental
|
196
|
+
def except(*keys)
|
197
|
+
Relation.new(@model, @term, **@options.except(*keys))
|
198
|
+
end
|
199
|
+
|
200
|
+
# experimental
|
201
|
+
def load
|
202
|
+
private_execute
|
203
|
+
self
|
204
|
+
end
|
205
|
+
|
206
|
+
def loaded?
|
207
|
+
!@execute.nil?
|
208
|
+
end
|
209
|
+
|
210
|
+
def respond_to_missing?(method_name, include_all)
|
211
|
+
Results.new(nil, nil, nil).respond_to?(method_name, include_all) || super
|
212
|
+
end
|
213
|
+
|
214
|
+
# TODO uncomment in 6.0
|
215
|
+
# def to_yaml
|
216
|
+
# private_execute.to_a.to_yaml
|
217
|
+
# end
|
218
|
+
|
219
|
+
private
|
220
|
+
|
221
|
+
def private_execute
|
222
|
+
@execute ||= query.execute
|
223
|
+
end
|
224
|
+
|
225
|
+
def query
|
226
|
+
@query ||= Query.new(@model, @term, **@options)
|
227
|
+
end
|
228
|
+
|
229
|
+
def check_loaded
|
230
|
+
raise Error, "Relation loaded" if loaded?
|
231
|
+
|
232
|
+
# reset query since options will change
|
233
|
+
@query = nil
|
234
|
+
end
|
235
|
+
|
236
|
+
# provides *very* basic protection from unfiltered parameters
|
237
|
+
# this is not meant to be comprehensive and may be expanded in the future
|
238
|
+
def ensure_permitted(obj)
|
239
|
+
obj.to_h
|
240
|
+
end
|
241
|
+
|
242
|
+
def initialize_copy(other)
|
243
|
+
super
|
244
|
+
@execute = nil
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|