dynamoid 3.2.0 → 3.6.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 +4 -4
- data/CHANGELOG.md +111 -1
- data/README.md +580 -241
- data/lib/dynamoid.rb +2 -0
- data/lib/dynamoid/adapter.rb +15 -15
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +82 -102
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +108 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +29 -16
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +3 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/backoff.rb +2 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +2 -3
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/start_key.rb +2 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +15 -6
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +15 -5
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +1 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +5 -3
- data/lib/dynamoid/application_time_zone.rb +1 -0
- data/lib/dynamoid/associations.rb +182 -19
- data/lib/dynamoid/associations/association.rb +4 -2
- data/lib/dynamoid/associations/belongs_to.rb +2 -1
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +2 -1
- data/lib/dynamoid/associations/has_many.rb +2 -1
- data/lib/dynamoid/associations/has_one.rb +2 -1
- data/lib/dynamoid/associations/many_association.rb +65 -22
- data/lib/dynamoid/associations/single_association.rb +28 -1
- data/lib/dynamoid/components.rb +8 -3
- data/lib/dynamoid/config.rb +16 -3
- data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +1 -0
- data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +1 -0
- data/lib/dynamoid/config/options.rb +1 -0
- data/lib/dynamoid/criteria.rb +2 -1
- data/lib/dynamoid/criteria/chain.rb +418 -46
- data/lib/dynamoid/criteria/ignored_conditions_detector.rb +3 -3
- data/lib/dynamoid/criteria/key_fields_detector.rb +109 -32
- data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +3 -2
- data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +1 -1
- data/lib/dynamoid/dirty.rb +239 -32
- data/lib/dynamoid/document.rb +130 -251
- data/lib/dynamoid/dumping.rb +9 -0
- data/lib/dynamoid/dynamodb_time_zone.rb +1 -0
- data/lib/dynamoid/fields.rb +246 -20
- data/lib/dynamoid/finders.rb +69 -32
- data/lib/dynamoid/identity_map.rb +6 -0
- data/lib/dynamoid/indexes.rb +76 -17
- data/lib/dynamoid/loadable.rb +31 -0
- data/lib/dynamoid/log/formatter.rb +26 -0
- data/lib/dynamoid/middleware/identity_map.rb +1 -0
- data/lib/dynamoid/persistence.rb +592 -122
- data/lib/dynamoid/persistence/import.rb +73 -0
- data/lib/dynamoid/persistence/save.rb +64 -0
- data/lib/dynamoid/persistence/update_fields.rb +63 -0
- data/lib/dynamoid/persistence/upsert.rb +60 -0
- data/lib/dynamoid/primary_key_type_mapping.rb +1 -0
- data/lib/dynamoid/railtie.rb +1 -0
- data/lib/dynamoid/tasks.rb +3 -1
- data/lib/dynamoid/tasks/database.rb +1 -0
- data/lib/dynamoid/type_casting.rb +12 -2
- data/lib/dynamoid/undumping.rb +8 -0
- data/lib/dynamoid/validations.rb +2 -0
- data/lib/dynamoid/version.rb +1 -1
- metadata +49 -71
- data/.coveralls.yml +0 -1
- data/.document +0 -5
- data/.gitignore +0 -74
- data/.rspec +0 -2
- data/.rubocop.yml +0 -71
- data/.rubocop_todo.yml +0 -55
- data/.travis.yml +0 -41
- data/Appraisals +0 -28
- data/Gemfile +0 -8
- data/Rakefile +0 -46
- data/Vagrantfile +0 -29
- data/docker-compose.yml +0 -7
- data/dynamoid.gemspec +0 -57
- data/gemfiles/rails_4_2.gemfile +0 -11
- data/gemfiles/rails_5_0.gemfile +0 -10
- data/gemfiles/rails_5_1.gemfile +0 -10
- data/gemfiles/rails_5_2.gemfile +0 -10
data/lib/dynamoid/criteria.rb
CHANGED
@@ -7,8 +7,9 @@ module Dynamoid
|
|
7
7
|
module Criteria
|
8
8
|
extend ActiveSupport::Concern
|
9
9
|
|
10
|
+
# @private
|
10
11
|
module ClassMethods
|
11
|
-
%i[where all first last each record_limit scan_limit batch start scan_index_forward find_by_pages].each do |meth|
|
12
|
+
%i[where all first last each record_limit scan_limit batch start scan_index_forward find_by_pages project pluck].each do |meth|
|
12
13
|
# Return a criteria chain in response to a method that will begin or end a chain. For more information,
|
13
14
|
# see Dynamoid::Criteria::Chain.
|
14
15
|
#
|
@@ -22,26 +22,74 @@ module Dynamoid
|
|
22
22
|
@consistent_read = false
|
23
23
|
@scan_index_forward = true
|
24
24
|
|
25
|
-
# Honor STI and :type field if it presents
|
26
|
-
type = @source.inheritance_field
|
27
|
-
if @source.attributes.key?(type)
|
28
|
-
@query[:"#{type}.in"] = @source.deep_subclasses.map(&:name) << @source.name
|
29
|
-
end
|
30
|
-
|
31
25
|
# we should re-initialize keys detector every time we change query
|
32
26
|
@key_fields_detector = KeyFieldsDetector.new(@query, @source)
|
33
27
|
end
|
34
28
|
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
29
|
+
# Returns a chain which is a result of filtering current chain with the specified conditions.
|
30
|
+
#
|
31
|
+
# It accepts conditions in the form of a hash.
|
32
|
+
#
|
33
|
+
# Post.where(links_count: 2)
|
34
|
+
#
|
35
|
+
# A key could be either string or symbol.
|
36
|
+
#
|
37
|
+
# In order to express conditions other than equality predicates could be used.
|
38
|
+
# Predicate should be added to an attribute name to form a key +'created_at.gt' => Date.yesterday+
|
39
|
+
#
|
40
|
+
# Currently supported following predicates:
|
41
|
+
# - +gt+ - greater than
|
42
|
+
# - +gte+ - greater or equal
|
43
|
+
# - +lt+ - less than
|
44
|
+
# - +lte+ - less or equal
|
45
|
+
# - +ne+ - not equal
|
46
|
+
# - +between+ - an attribute value is greater than the first value and less than the second value
|
47
|
+
# - +in+ - check an attribute in a list of values
|
48
|
+
# - +begins_with+ - check for a prefix in string
|
49
|
+
# - +contains+ - check substring or value in a set or array
|
50
|
+
# - +not_contains+ - check for absence of substring or a value in set or array
|
51
|
+
# - +null+ - attribute doesn't exists in an item
|
52
|
+
# - +not_null+ - attribute exists in an item
|
53
|
+
#
|
54
|
+
# All the predicates match operators supported by DynamoDB's
|
55
|
+
# {ComparisonOperator}[https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html#DDB-Type-Condition-ComparisonOperator]
|
56
|
+
#
|
57
|
+
# Post.where('size.gt' => 1000)
|
58
|
+
# Post.where('size.gte' => 1000)
|
59
|
+
# Post.where('size.lt' => 35000)
|
60
|
+
# Post.where('size.lte' => 35000)
|
61
|
+
# Post.where('author.ne' => 'John Doe')
|
62
|
+
# Post.where('created_at.between' => [Time.now - 3600, Time.now])
|
63
|
+
# Post.where('category.in' => ['tech', 'fashion'])
|
64
|
+
# Post.where('title.begins_with' => 'How long')
|
65
|
+
# Post.where('tags.contains' => 'Ruby')
|
66
|
+
# Post.where('tags.not_contains' => 'Ruby on Rails')
|
67
|
+
# Post.where('legacy_attribute.null' => true)
|
68
|
+
# Post.where('optional_attribute.not_null' => true)
|
69
|
+
#
|
70
|
+
# There are some limitations for a sort key. Only following predicates
|
71
|
+
# are supported - +gt+, +gte+, +lt+, +lte+, +between+, +begins_with+.
|
72
|
+
#
|
73
|
+
# +where+ without argument will return the current chain.
|
74
|
+
#
|
75
|
+
# Multiple calls can be chained together and conditions will be merged:
|
76
|
+
#
|
77
|
+
# Post.where('size.gt' => 1000).where('title' => 'some title')
|
78
|
+
#
|
79
|
+
# It's equivalent to:
|
80
|
+
#
|
81
|
+
# Post.where('size.gt' => 1000, 'title' => 'some title')
|
38
82
|
#
|
39
|
-
#
|
40
|
-
#
|
83
|
+
# But only one condition can be specified for a certain attribute. The
|
84
|
+
# last specified condition will override all the others. Only condition
|
85
|
+
# 'size.lt' => 200 will be used in following examples:
|
41
86
|
#
|
42
|
-
#
|
43
|
-
# where(
|
87
|
+
# Post.where('size.gt' => 100, 'size.lt' => 200)
|
88
|
+
# Post.where('size.gt' => 100).where('size.lt' => 200)
|
44
89
|
#
|
90
|
+
# Internally +where+ performs either +Scan+ or +Query+ operation.
|
91
|
+
#
|
92
|
+
# @return [Dynamoid::Criteria::Chain]
|
45
93
|
# @since 0.2.0
|
46
94
|
def where(args)
|
47
95
|
detector = IgnoredConditionsDetector.new(args)
|
@@ -67,6 +115,13 @@ module Dynamoid
|
|
67
115
|
self
|
68
116
|
end
|
69
117
|
|
118
|
+
# Turns on strongly consistent reads.
|
119
|
+
#
|
120
|
+
# By default reads are eventually consistent.
|
121
|
+
#
|
122
|
+
# Post.where('size.gt' => 1000).consistent
|
123
|
+
#
|
124
|
+
# @return [Dynamoid::Criteria::Chain]
|
70
125
|
def consistent
|
71
126
|
@consistent_read = true
|
72
127
|
self
|
@@ -74,11 +129,39 @@ module Dynamoid
|
|
74
129
|
|
75
130
|
# Returns all the records matching the criteria.
|
76
131
|
#
|
132
|
+
# Since +where+ and most of the other methods return a +Chain+
|
133
|
+
# the only way to get a result as a collection is to call the +all+
|
134
|
+
# method. It returns +Enumerator+ which could be used directly or
|
135
|
+
# transformed into +Array+
|
136
|
+
#
|
137
|
+
# Post.all # => Enumerator
|
138
|
+
# Post.where(links_count: 2).all # => Enumerator
|
139
|
+
# Post.where(links_count: 2).all.to_a # => Array
|
140
|
+
#
|
141
|
+
# When the result set is too large DynamoDB divides it into separate
|
142
|
+
# pages. While an enumerator iterates over the result models each page
|
143
|
+
# is loaded lazily. So even an extra large result set can be loaded and
|
144
|
+
# processed with considerably small memory footprint and throughput
|
145
|
+
# consumption.
|
146
|
+
#
|
147
|
+
# @return [Enumerator::Lazy]
|
77
148
|
# @since 0.2.0
|
78
149
|
def all
|
79
150
|
records
|
80
151
|
end
|
81
152
|
|
153
|
+
# Returns the actual number of items in a table matching the criteria.
|
154
|
+
#
|
155
|
+
# Post.where(links_count: 2).count
|
156
|
+
#
|
157
|
+
# Internally it uses either `Scan` or `Query` DynamoDB's operation so it
|
158
|
+
# costs like all the matching items were read from a table.
|
159
|
+
#
|
160
|
+
# The only difference is that items are read by DynemoDB but not actually
|
161
|
+
# loaded on the client side. DynamoDB returns only count of items after
|
162
|
+
# filtering.
|
163
|
+
#
|
164
|
+
# @return [Integer]
|
82
165
|
def count
|
83
166
|
if @key_fields_detector.key_present?
|
84
167
|
count_via_query
|
@@ -87,27 +170,72 @@ module Dynamoid
|
|
87
170
|
end
|
88
171
|
end
|
89
172
|
|
90
|
-
# Returns the
|
91
|
-
#
|
92
|
-
#
|
173
|
+
# Returns the first item matching the criteria.
|
174
|
+
#
|
175
|
+
# Post.where(links_count: 2).first
|
176
|
+
#
|
177
|
+
# Applies `record_limit(1)` to ensure only a single record is fetched
|
178
|
+
# when no non-key conditions are present and `scan_limit(1)` when no
|
179
|
+
# conditions are present at all.
|
93
180
|
#
|
181
|
+
# If used without criteria it just returns the first item of some
|
182
|
+
# arbitrary order.
|
183
|
+
#
|
184
|
+
# Post.first
|
185
|
+
#
|
186
|
+
# @return [Model|nil]
|
187
|
+
def first(*args)
|
188
|
+
n = args.first || 1
|
189
|
+
|
190
|
+
return scan_limit(n).to_a.first(*args) if @query.blank?
|
191
|
+
return super if @key_fields_detector.non_key_present?
|
192
|
+
|
193
|
+
record_limit(n).to_a.first(*args)
|
194
|
+
end
|
195
|
+
|
196
|
+
# Returns the last item matching the criteria.
|
197
|
+
#
|
198
|
+
# Post.where(links_count: 2).last
|
199
|
+
#
|
200
|
+
# DynamoDB doesn't support ordering by some arbitrary attribute except a
|
201
|
+
# sort key. So this method is mostly useful during development and
|
202
|
+
# testing.
|
203
|
+
#
|
204
|
+
# If used without criteria it just returns the last item of some arbitrary order.
|
205
|
+
#
|
206
|
+
# Post.last
|
207
|
+
#
|
208
|
+
# It isn't efficient from the performance point of view as far as it reads and
|
209
|
+
# loads all the filtered items from DynamoDB.
|
210
|
+
#
|
211
|
+
# @return [Model|nil]
|
94
212
|
def last
|
95
213
|
all.to_a.last
|
96
214
|
end
|
97
215
|
|
98
|
-
#
|
216
|
+
# Deletes all the items matching the criteria.
|
217
|
+
#
|
218
|
+
# Post.where(links_count: 2).delete_all
|
99
219
|
#
|
220
|
+
# If called without criteria then it deletes all the items in a table.
|
221
|
+
#
|
222
|
+
# Post.delete_all
|
223
|
+
#
|
224
|
+
# It loads all the items either with +Scan+ or +Query+ operation and
|
225
|
+
# deletes them in batch with +BatchWriteItem+ operation. +BatchWriteItem+
|
226
|
+
# is limited by request size and items count so it's quite possible the
|
227
|
+
# deletion will require several +BatchWriteItem+ calls.
|
100
228
|
def delete_all
|
101
229
|
ids = []
|
102
230
|
ranges = []
|
103
231
|
|
104
232
|
if @key_fields_detector.key_present?
|
105
|
-
Dynamoid.adapter.query(source.table_name, range_query).flat_map{ |i| i }.collect do |hash|
|
233
|
+
Dynamoid.adapter.query(source.table_name, range_query).flat_map { |i| i }.collect do |hash|
|
106
234
|
ids << hash[source.hash_key.to_sym]
|
107
235
|
ranges << hash[source.range_key.to_sym] if source.range_key
|
108
236
|
end
|
109
237
|
else
|
110
|
-
Dynamoid.adapter.scan(source.table_name, scan_query, scan_opts).flat_map{ |i| i }.collect do |hash|
|
238
|
+
Dynamoid.adapter.scan(source.table_name, scan_query, scan_opts).flat_map { |i| i }.collect do |hash|
|
111
239
|
ids << hash[source.hash_key.to_sym]
|
112
240
|
ranges << hash[source.range_key.to_sym] if source.range_key
|
113
241
|
end
|
@@ -117,48 +245,229 @@ module Dynamoid
|
|
117
245
|
end
|
118
246
|
alias destroy_all delete_all
|
119
247
|
|
120
|
-
#
|
121
|
-
#
|
248
|
+
# Set the record limit.
|
249
|
+
#
|
250
|
+
# The record limit is the limit of evaluated items returned by the
|
251
|
+
# +Query+ or +Scan+. In other words it's how many items should be
|
252
|
+
# returned in response.
|
253
|
+
#
|
254
|
+
# Post.where(links_count: 2).record_limit(1000) # => 1000 models
|
255
|
+
# Post.record_limit(1000) # => 1000 models
|
256
|
+
#
|
257
|
+
# It could be very inefficient in terms of HTTP requests in pathological
|
258
|
+
# cases. DynamoDB doesn't support out of the box the limits for items
|
259
|
+
# count after filtering. So it's possible to make a lot of HTTP requests
|
260
|
+
# to find items matching criteria and skip not matching. It means that
|
261
|
+
# the cost (read capacity units) is unpredictable.
|
262
|
+
#
|
263
|
+
# Because of such issues with performance and cost it's mostly useful in
|
264
|
+
# development and testing.
|
265
|
+
#
|
266
|
+
# When called without criteria it works like +scan_limit+.
|
267
|
+
#
|
268
|
+
# @return [Dynamoid::Criteria::Chain]
|
122
269
|
def record_limit(limit)
|
123
270
|
@record_limit = limit
|
124
271
|
self
|
125
272
|
end
|
126
273
|
|
127
|
-
#
|
128
|
-
#
|
129
|
-
#
|
130
|
-
#
|
274
|
+
# Set the scan limit.
|
275
|
+
#
|
276
|
+
# The scan limit is the limit of records that DynamoDB will internally
|
277
|
+
# read with +Query+ or +Scan+. It's different from the record limit as
|
278
|
+
# with filtering DynamoDB may look at N scanned items but return 0
|
279
|
+
# items if none passes the filter. So it can return less items than was
|
280
|
+
# specified with the limit.
|
281
|
+
#
|
282
|
+
# Post.where(links_count: 2).scan_limit(1000) # => 850 models
|
283
|
+
# Post.scan_limit(1000) # => 1000 models
|
284
|
+
#
|
285
|
+
# By contrast with +record_limit+ the cost (read capacity units) and
|
286
|
+
# performance is predictable.
|
287
|
+
#
|
288
|
+
# When called without criteria it works like +record_limit+.
|
289
|
+
#
|
290
|
+
# @return [Dynamoid::Criteria::Chain]
|
131
291
|
def scan_limit(limit)
|
132
292
|
@scan_limit = limit
|
133
293
|
self
|
134
294
|
end
|
135
295
|
|
296
|
+
# Set the batch size.
|
297
|
+
#
|
298
|
+
# The batch size is a number of items which will be lazily loaded one by one.
|
299
|
+
# When the batch size is set then items will be loaded batch by batch of
|
300
|
+
# the specified size instead of relying on the default paging mechanism
|
301
|
+
# of DynamoDB.
|
302
|
+
#
|
303
|
+
# Post.where(links_count: 2).batch(1000).all.each do |post|
|
304
|
+
# # process a post
|
305
|
+
# end
|
306
|
+
#
|
307
|
+
# It's useful to limit memory usage or throughput consumption
|
308
|
+
#
|
309
|
+
# @return [Dynamoid::Criteria::Chain]
|
136
310
|
def batch(batch_size)
|
137
311
|
@batch_size = batch_size
|
138
312
|
self
|
139
313
|
end
|
140
314
|
|
315
|
+
# Set the start item.
|
316
|
+
#
|
317
|
+
# When the start item is set the items will be loaded starting right
|
318
|
+
# after the specified item.
|
319
|
+
#
|
320
|
+
# Post.where(links_count: 2).start(post)
|
321
|
+
#
|
322
|
+
# It can be used to implement an own pagination mechanism.
|
323
|
+
#
|
324
|
+
# Post.where(author_id: author_id).start(last_post).scan_limit(50)
|
325
|
+
#
|
326
|
+
# The specified start item will not be returned back in a result set.
|
327
|
+
#
|
328
|
+
# Actually it doesn't need all the item attributes to start - an item may
|
329
|
+
# have only the primary key attributes (partition and sort key if it's
|
330
|
+
# declared).
|
331
|
+
#
|
332
|
+
# Post.where(links_count: 2).start(Post.new(id: id))
|
333
|
+
#
|
334
|
+
# It also supports a +Hash+ argument with the keys attributes - a
|
335
|
+
# partition key and a sort key (if it's declared).
|
336
|
+
#
|
337
|
+
# Post.where(links_count: 2).start(id: id)
|
338
|
+
#
|
339
|
+
# @return [Dynamoid::Criteria::Chain]
|
141
340
|
def start(start)
|
142
341
|
@start = start
|
143
342
|
self
|
144
343
|
end
|
145
344
|
|
345
|
+
# Reverse the sort order.
|
346
|
+
#
|
347
|
+
# By default the sort order is ascending (by the sort key value). Set a
|
348
|
+
# +false+ value to reverse the order.
|
349
|
+
#
|
350
|
+
# Post.where(id: id, 'views_count.gt' => 1000).scan_index_forward(false)
|
351
|
+
#
|
352
|
+
# It works only for queries with a partition key condition e.g. +id:
|
353
|
+
# 'some-id'+ which internally performs +Query+ operation.
|
354
|
+
#
|
355
|
+
# @return [Dynamoid::Criteria::Chain]
|
146
356
|
def scan_index_forward(scan_index_forward)
|
147
357
|
@scan_index_forward = scan_index_forward
|
148
358
|
self
|
149
359
|
end
|
150
360
|
|
151
|
-
# Allows
|
361
|
+
# Allows to use the results of a search as an enumerable over the results
|
362
|
+
# found.
|
363
|
+
#
|
364
|
+
# Post.each do |post|
|
365
|
+
# end
|
366
|
+
#
|
367
|
+
# Post.all.each do |post|
|
368
|
+
# end
|
369
|
+
#
|
370
|
+
# Post.where(links_count: 2).each do |post|
|
371
|
+
# end
|
372
|
+
#
|
373
|
+
# It works similar to the +all+ method so results are loaded lazily.
|
152
374
|
#
|
153
375
|
# @since 0.2.0
|
154
376
|
def each(&block)
|
155
377
|
records.each(&block)
|
156
378
|
end
|
157
379
|
|
380
|
+
# Iterates over the pages returned by DynamoDB.
|
381
|
+
#
|
382
|
+
# DynamoDB has its own paging machanism and divides a large result set
|
383
|
+
# into separate pages. The +find_by_pages+ method provides access to
|
384
|
+
# these native DynamoDB pages.
|
385
|
+
#
|
386
|
+
# The pages are loaded lazily.
|
387
|
+
#
|
388
|
+
# Post.where('views_count.gt' => 1000).find_by_pages do |posts, options|
|
389
|
+
# # process posts
|
390
|
+
# end
|
391
|
+
#
|
392
|
+
# It passes as block argument an +Array+ of models and a Hash with options.
|
393
|
+
#
|
394
|
+
# Options +Hash+ contains only one option +:last_evaluated_key+. The last
|
395
|
+
# evaluated key is a Hash with key attributes of the last item processed by
|
396
|
+
# DynamoDB. It can be used to resume querying using the +start+ method.
|
397
|
+
#
|
398
|
+
# posts, options = Post.where('views_count.gt' => 1000).find_by_pages.first
|
399
|
+
# last_key = options[:last_evaluated_key]
|
400
|
+
#
|
401
|
+
# # ...
|
402
|
+
#
|
403
|
+
# Post.where('views_count.gt' => 1000).start(last_key).find_by_pages do |posts, options|
|
404
|
+
# end
|
405
|
+
#
|
406
|
+
# If it's called without a block then it returns an +Enumerator+.
|
407
|
+
#
|
408
|
+
# enum = Post.where('views_count.gt' => 1000).find_by_pages
|
409
|
+
#
|
410
|
+
# enum.each do |posts, options|
|
411
|
+
# # process posts
|
412
|
+
# end
|
413
|
+
#
|
414
|
+
# @return [Enumerator::Lazy]
|
158
415
|
def find_by_pages(&block)
|
159
416
|
pages.each(&block)
|
160
417
|
end
|
161
418
|
|
419
|
+
# Select only specified fields.
|
420
|
+
#
|
421
|
+
# It takes one or more field names and returns a collection of models with only
|
422
|
+
# these fields set.
|
423
|
+
#
|
424
|
+
# Post.where('views_count.gt' => 1000).select(:title)
|
425
|
+
# Post.where('views_count.gt' => 1000).select(:title, :created_at)
|
426
|
+
# Post.select(:id)
|
427
|
+
#
|
428
|
+
# It can be used to avoid loading large field values and to decrease a
|
429
|
+
# memory footprint.
|
430
|
+
#
|
431
|
+
# @return [Dynamoid::Criteria::Chain]
|
432
|
+
def project(*fields)
|
433
|
+
@project = fields.map(&:to_sym)
|
434
|
+
self
|
435
|
+
end
|
436
|
+
|
437
|
+
# Select only specified fields.
|
438
|
+
#
|
439
|
+
# It takes one or more field names and returns an array of either values
|
440
|
+
# or arrays of values.
|
441
|
+
#
|
442
|
+
# Post.pluck(:id) # => ['1', '2']
|
443
|
+
# Post.pluck(:title, :title) # => [['1', 'Title #1'], ['2', 'Title#2']]
|
444
|
+
#
|
445
|
+
# Post.where('views_count.gt' => 1000).pluck(:title)
|
446
|
+
#
|
447
|
+
# There are some differences between +pluck+ and +project+. +pluck+
|
448
|
+
# - doesn't instantiate models
|
449
|
+
# - it isn't chainable and returns +Array+ instead of +Chain+
|
450
|
+
#
|
451
|
+
# It deserializes values if a field type isn't supported by DynamoDB natively.
|
452
|
+
#
|
453
|
+
# It can be used to avoid loading large field values and to decrease a
|
454
|
+
# memory footprint.
|
455
|
+
#
|
456
|
+
# @return [Array]
|
457
|
+
def pluck(*args)
|
458
|
+
fields = args.map(&:to_sym)
|
459
|
+
@project = fields
|
460
|
+
|
461
|
+
if fields.many?
|
462
|
+
items.map do |item|
|
463
|
+
fields.map { |key| Undumping.undump_field(item[key], source.attributes[key]) }
|
464
|
+
end.to_a
|
465
|
+
else
|
466
|
+
key = fields.first
|
467
|
+
items.map { |item| Undumping.undump_field(item[key], source.attributes[key]) }.to_a
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
162
471
|
private
|
163
472
|
|
164
473
|
# The actual records referenced by the association.
|
@@ -167,7 +476,12 @@ module Dynamoid
|
|
167
476
|
#
|
168
477
|
# @since 0.2.0
|
169
478
|
def records
|
170
|
-
pages.lazy.flat_map { |
|
479
|
+
pages.lazy.flat_map { |items, _| items }
|
480
|
+
end
|
481
|
+
|
482
|
+
# Raw items like they are stored before type casting
|
483
|
+
def items
|
484
|
+
raw_pages.lazy.flat_map { |items, _| items }
|
171
485
|
end
|
172
486
|
|
173
487
|
# Arrays of records, sized based on the actual pages produced by DynamoDB
|
@@ -176,11 +490,19 @@ module Dynamoid
|
|
176
490
|
#
|
177
491
|
# @since 3.1.0
|
178
492
|
def pages
|
493
|
+
raw_pages.lazy.map do |items, options|
|
494
|
+
models = items.map { |i| source.from_database(i) }
|
495
|
+
[models, options]
|
496
|
+
end.each
|
497
|
+
end
|
498
|
+
|
499
|
+
# Pages of items before type casting
|
500
|
+
def raw_pages
|
179
501
|
if @key_fields_detector.key_present?
|
180
|
-
|
502
|
+
raw_pages_via_query
|
181
503
|
else
|
182
504
|
issue_scan_warning if Dynamoid::Config.warn_on_scan && query.present?
|
183
|
-
|
505
|
+
raw_pages_via_scan
|
184
506
|
end
|
185
507
|
end
|
186
508
|
|
@@ -189,10 +511,12 @@ module Dynamoid
|
|
189
511
|
# @return [Enumerator] an iterator of the found pages. An array of records
|
190
512
|
#
|
191
513
|
# @since 3.1.0
|
192
|
-
def
|
193
|
-
Enumerator.new do |
|
514
|
+
def raw_pages_via_query
|
515
|
+
Enumerator.new do |y|
|
194
516
|
Dynamoid.adapter.query(source.table_name, range_query).each do |items, metadata|
|
195
|
-
|
517
|
+
options = metadata.slice(:last_evaluated_key)
|
518
|
+
|
519
|
+
y.yield items, options
|
196
520
|
end
|
197
521
|
end
|
198
522
|
end
|
@@ -202,10 +526,12 @@ module Dynamoid
|
|
202
526
|
# @return [Enumerator] an iterator of the found pages. An array of records
|
203
527
|
#
|
204
528
|
# @since 3.1.0
|
205
|
-
def
|
206
|
-
Enumerator.new do |
|
529
|
+
def raw_pages_via_scan
|
530
|
+
Enumerator.new do |y|
|
207
531
|
Dynamoid.adapter.scan(source.table_name, scan_query, scan_opts).each do |items, metadata|
|
208
|
-
|
532
|
+
options = metadata.slice(:last_evaluated_key)
|
533
|
+
|
534
|
+
y.yield items, options
|
209
535
|
end
|
210
536
|
end
|
211
537
|
end
|
@@ -271,6 +597,13 @@ module Dynamoid
|
|
271
597
|
{ contains: val }
|
272
598
|
when 'not_contains'
|
273
599
|
{ not_contains: val }
|
600
|
+
# NULL/NOT_NULL operators don't have parameters
|
601
|
+
# So { null: true } means NULL check and { null: false } means NOT_NULL one
|
602
|
+
# The same logic is used for { not_null: BOOL }
|
603
|
+
when 'null'
|
604
|
+
val ? { null: nil } : { not_null: nil }
|
605
|
+
when 'not_null'
|
606
|
+
val ? { not_null: nil } : { null: nil }
|
274
607
|
end
|
275
608
|
|
276
609
|
{ name.to_sym => hash }
|
@@ -282,6 +615,13 @@ module Dynamoid
|
|
282
615
|
|
283
616
|
def range_query
|
284
617
|
opts = {}
|
618
|
+
query = self.query
|
619
|
+
|
620
|
+
# Honor STI and :type field if it presents
|
621
|
+
if @source.attributes.key?(@source.inheritance_field) &&
|
622
|
+
@key_fields_detector.hash_key.to_sym != @source.inheritance_field.to_sym
|
623
|
+
query.update(sti_condition)
|
624
|
+
end
|
285
625
|
|
286
626
|
# Add hash key
|
287
627
|
opts[:hash_key] = @key_fields_detector.hash_key
|
@@ -289,15 +629,7 @@ module Dynamoid
|
|
289
629
|
|
290
630
|
# Add range key
|
291
631
|
if @key_fields_detector.range_key
|
292
|
-
opts
|
293
|
-
if query[@key_fields_detector.range_key].present?
|
294
|
-
value = type_cast_condition_parameter(@key_fields_detector.range_key, query[@key_fields_detector.range_key])
|
295
|
-
opts.update(range_eq: value)
|
296
|
-
end
|
297
|
-
|
298
|
-
query.keys.select { |k| k.to_s =~ /^#{@key_fields_detector.range_key}\./ }.each do |key|
|
299
|
-
opts.merge!(range_hash(key))
|
300
|
-
end
|
632
|
+
add_range_key_to_range_query(query, opts)
|
301
633
|
end
|
302
634
|
|
303
635
|
(query.keys.map(&:to_sym) - [@key_fields_detector.hash_key.to_sym, @key_fields_detector.range_key.try(:to_sym)])
|
@@ -314,10 +646,27 @@ module Dynamoid
|
|
314
646
|
opts.merge(query_opts).merge(consistent_opts)
|
315
647
|
end
|
316
648
|
|
649
|
+
def add_range_key_to_range_query(query, opts)
|
650
|
+
opts[:range_key] = @key_fields_detector.range_key
|
651
|
+
if query[@key_fields_detector.range_key].present?
|
652
|
+
value = type_cast_condition_parameter(@key_fields_detector.range_key, query[@key_fields_detector.range_key])
|
653
|
+
opts.update(range_eq: value)
|
654
|
+
end
|
655
|
+
|
656
|
+
query.keys.select { |k| k.to_s =~ /^#{@key_fields_detector.range_key}\./ }.each do |key|
|
657
|
+
opts.merge!(range_hash(key))
|
658
|
+
end
|
659
|
+
end
|
660
|
+
|
661
|
+
# TODO: casting should be operator aware
|
662
|
+
# e.g. for NULL operator value should be boolean
|
663
|
+
# and isn't related to an attribute own type
|
317
664
|
def type_cast_condition_parameter(key, value)
|
318
665
|
return value if %i[array set].include?(source.attributes[key.to_sym][:type])
|
319
666
|
|
320
|
-
if
|
667
|
+
if [true, false].include?(value) # Support argument for null/not_null operators
|
668
|
+
value
|
669
|
+
elsif !value.respond_to?(:to_ary)
|
321
670
|
options = source.attributes[key.to_sym]
|
322
671
|
value_casted = TypeCasting.cast_field(value, options)
|
323
672
|
Dumping.dump_field(value_casted, options)
|
@@ -356,17 +705,27 @@ module Dynamoid
|
|
356
705
|
|
357
706
|
def query_opts
|
358
707
|
opts = {}
|
708
|
+
# Don't specify select = ALL_ATTRIBUTES option explicitly because it's
|
709
|
+
# already a default value of Select statement. Explicite Select value
|
710
|
+
# conflicts with AttributesToGet statement (project option).
|
359
711
|
opts[:index_name] = @key_fields_detector.index_name if @key_fields_detector.index_name
|
360
|
-
opts[:select] = 'ALL_ATTRIBUTES'
|
361
712
|
opts[:record_limit] = @record_limit if @record_limit
|
362
713
|
opts[:scan_limit] = @scan_limit if @scan_limit
|
363
714
|
opts[:batch_size] = @batch_size if @batch_size
|
364
715
|
opts[:exclusive_start_key] = start_key if @start
|
365
716
|
opts[:scan_index_forward] = @scan_index_forward
|
717
|
+
opts[:project] = @project
|
366
718
|
opts
|
367
719
|
end
|
368
720
|
|
369
721
|
def scan_query
|
722
|
+
query = self.query
|
723
|
+
|
724
|
+
# Honor STI and :type field if it presents
|
725
|
+
if sti_condition
|
726
|
+
query.update(sti_condition)
|
727
|
+
end
|
728
|
+
|
370
729
|
{}.tap do |opts|
|
371
730
|
query.keys.map(&:to_sym).each do |key|
|
372
731
|
if key.to_s.include?('.')
|
@@ -386,8 +745,21 @@ module Dynamoid
|
|
386
745
|
opts[:batch_size] = @batch_size if @batch_size
|
387
746
|
opts[:exclusive_start_key] = start_key if @start
|
388
747
|
opts[:consistent_read] = true if @consistent_read
|
748
|
+
opts[:project] = @project
|
389
749
|
opts
|
390
750
|
end
|
751
|
+
|
752
|
+
def sti_condition
|
753
|
+
condition = {}
|
754
|
+
type = @source.inheritance_field
|
755
|
+
|
756
|
+
if @source.attributes.key?(type)
|
757
|
+
class_names = @source.deep_subclasses.map(&:name) << @source.name
|
758
|
+
condition[:"#{type}.in"] = class_names
|
759
|
+
end
|
760
|
+
|
761
|
+
condition
|
762
|
+
end
|
391
763
|
end
|
392
764
|
end
|
393
765
|
end
|