aws-sdk 1.2.6 → 1.3.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 (60) hide show
  1. data/lib/aws.rb +2 -0
  2. data/lib/aws/api_config/DynamoDB-2011-12-05.yml +721 -0
  3. data/lib/aws/core.rb +10 -1
  4. data/lib/aws/core/client.rb +17 -12
  5. data/lib/aws/core/configuration.rb +13 -3
  6. data/lib/aws/core/configured_json_client_methods.rb +71 -0
  7. data/lib/aws/core/lazy_error_classes.rb +7 -2
  8. data/lib/aws/core/option_grammar.rb +67 -13
  9. data/lib/aws/core/resource.rb +9 -1
  10. data/lib/aws/core/session_signer.rb +95 -0
  11. data/lib/aws/dynamo_db.rb +169 -0
  12. data/lib/aws/dynamo_db/attribute_collection.rb +460 -0
  13. data/lib/aws/dynamo_db/batch_get.rb +206 -0
  14. data/lib/aws/dynamo_db/client.rb +119 -0
  15. data/lib/aws/dynamo_db/config.rb +20 -0
  16. data/lib/aws/dynamo_db/errors.rb +57 -0
  17. data/lib/aws/dynamo_db/expectations.rb +40 -0
  18. data/lib/aws/dynamo_db/item.rb +130 -0
  19. data/lib/aws/dynamo_db/item_collection.rb +837 -0
  20. data/lib/aws/{record/optimistic_locking.rb → dynamo_db/item_data.rb} +9 -12
  21. data/lib/aws/{record/attributes/boolean.rb → dynamo_db/keys.rb} +15 -23
  22. data/lib/aws/dynamo_db/primary_key_element.rb +47 -0
  23. data/lib/aws/dynamo_db/request.rb +78 -0
  24. data/lib/aws/{record/attributes/float.rb → dynamo_db/resource.rb} +10 -25
  25. data/lib/aws/dynamo_db/table.rb +418 -0
  26. data/lib/aws/dynamo_db/table_collection.rb +165 -0
  27. data/lib/aws/dynamo_db/types.rb +86 -0
  28. data/lib/aws/ec2/resource_tag_collection.rb +3 -1
  29. data/lib/aws/record.rb +36 -8
  30. data/lib/aws/record/abstract_base.rb +642 -0
  31. data/lib/aws/record/attributes.rb +384 -0
  32. data/lib/aws/record/dirty_tracking.rb +0 -1
  33. data/lib/aws/record/errors.rb +0 -8
  34. data/lib/aws/record/hash_model.rb +163 -0
  35. data/lib/aws/record/hash_model/attributes.rb +182 -0
  36. data/lib/aws/record/hash_model/finder_methods.rb +178 -0
  37. data/lib/aws/record/hash_model/scope.rb +108 -0
  38. data/lib/aws/record/model.rb +429 -0
  39. data/lib/aws/record/model/attributes.rb +377 -0
  40. data/lib/aws/record/model/finder_methods.rb +232 -0
  41. data/lib/aws/record/model/scope.rb +213 -0
  42. data/lib/aws/record/scope.rb +43 -169
  43. data/lib/aws/record/validations.rb +11 -11
  44. data/lib/aws/s3/client.rb +9 -6
  45. data/lib/aws/s3/object_collection.rb +1 -1
  46. data/lib/aws/simple_db/expect_condition_option.rb +1 -1
  47. data/lib/aws/simple_db/item_collection.rb +5 -3
  48. data/lib/aws/sts/client.rb +9 -0
  49. metadata +73 -30
  50. data/lib/aws/record/attribute.rb +0 -94
  51. data/lib/aws/record/attribute_macros.rb +0 -312
  52. data/lib/aws/record/attributes/date.rb +0 -89
  53. data/lib/aws/record/attributes/datetime.rb +0 -86
  54. data/lib/aws/record/attributes/integer.rb +0 -68
  55. data/lib/aws/record/attributes/sortable_float.rb +0 -60
  56. data/lib/aws/record/attributes/sortable_integer.rb +0 -95
  57. data/lib/aws/record/attributes/string.rb +0 -69
  58. data/lib/aws/record/base.rb +0 -828
  59. data/lib/aws/record/finder_methods.rb +0 -230
  60. data/lib/aws/record/scopes.rb +0 -55
@@ -0,0 +1,837 @@
1
+ # Copyright 2011-2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://aws.amazon.com/apache2.0/
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+ module AWS
15
+ class DynamoDB
16
+
17
+ # Represents a collection of DynamoDB items.
18
+ #
19
+ # You can use an item collection to:
20
+ #
21
+ # * Create an {Item}
22
+ # * Get an {Item}
23
+ # * Enumerate {Item} or {ItemData} objects
24
+ #
25
+ # == Creating an Item
26
+ #
27
+ # To create an item, just call {#create} with a hash of attributes.
28
+ #
29
+ # table = dynamo_db.tables['my-table']
30
+ # table.hash_key = [:id, :string]
31
+ #
32
+ # table.items.create('id' => 'abc', 'count' => 5, 'colors' => %w(red blue))
33
+ #
34
+ # Attribute names can be symbols/strings and values can be strings or
35
+ # numbers or arrays/sets of strings/numbers. The attributes must contain
36
+ # the hash key name/value for the item and the value must be of the
37
+ # correct type (e.g. string or number).
38
+ #
39
+ # == Getting an Item
40
+ #
41
+ # To get an item, you provide the hash key
42
+ #
43
+ # # gets a reference to the item, no request is made
44
+ # item = table.items['hash-key-value']
45
+ #
46
+ # You call methods against the item returned to get, add, update or delete
47
+ # attributes. See {Item} for more information.
48
+ #
49
+ # == Enumerating Items
50
+ #
51
+ # You can enumerate items 2 ways:
52
+ #
53
+ # * Enuemrate {Item} objects
54
+ # * Enumerate {ItemData} objects
55
+ #
56
+ # {Item} objects do not have any attribute data populated. Think of
57
+ # them as just references to the item in Amazon DynamoDB. They only
58
+ # konw the objects hash key (and optional range key).
59
+ #
60
+ # {ItemData} objects are wrappers around the actual item attributes.
61
+ #
62
+ # To enumerate {Item} objects just call each on the item collection.
63
+ #
64
+ # table.items.each do |item|
65
+ # puts item.hash_value
66
+ # end
67
+ #
68
+ # To enumerate {ItemData} objects you need to specify what attributes
69
+ # you are interested in. This will cause #each to yield {ItemData}
70
+ # objects. Call {ItemData#attributes} to get the hash of attribute
71
+ # names/values.
72
+ #
73
+ # table.items.select('id', 'category').each do |item_data|
74
+ # item_data.attributes #=> { 'id' => 'abc', 'category' => 'foo' }
75
+ # end
76
+ #
77
+ # If you want item data objects with all attributes just call select
78
+ # without any arguments:
79
+ #
80
+ # # request a maximum of 10 items from Amazon DynamoDB
81
+ # table.items.select.limit(10).each do |item_data|
82
+ # item_data.attributes #=> { 'id' => 'abc', 'category' => 'foo', ... }
83
+ # end
84
+ #
85
+ # Please note that enumerating objects is done via the scan operation.
86
+ # Refer to the Amazon DynamoDB documentation for more information
87
+ # about scanning.
88
+ #
89
+ class ItemCollection
90
+
91
+ include Core::Collection::Limitable
92
+ include Types
93
+ include Expectations
94
+
95
+ # @private
96
+ def initialize(table, opts = {})
97
+ @table = table
98
+ @scan_filters = opts[:scan_filters] || {}
99
+ super
100
+ end
101
+
102
+ # @return [Table] The table to which the items in the collection
103
+ # belong.
104
+ attr_reader :table
105
+
106
+ # @private
107
+ attr_reader :scan_filters
108
+
109
+ # Creates a new item, or replaces an old item with a new item
110
+ # (including all the attributes). If an item already exists in
111
+ # the specified table with the same primary key, the new item
112
+ # completely replaces the existing item. You can perform a
113
+ # conditional put (insert a new item if one with the specified
114
+ # primary key doesn't exist), or replace an existing item if it
115
+ # has certain attribute values.
116
+ #
117
+ # items.put(:id => "abc123", :colors => ["red", "white"])
118
+ #
119
+ # @param [Hash] attributes The attributes to store with the
120
+ # item. These must include the primary key attributes for the
121
+ # table (see {Table#hash_key} and {Table#range_key}.
122
+ # Attribute names may be symbols or UTF-8 strings, and
123
+ # attribute values may be any of these types:
124
+ #
125
+ # * String
126
+ # * Array<String> or Set<String>
127
+ # * Numeric
128
+ # * Array<Numeric> or Set<Numeric>
129
+ #
130
+ # Empty sets, arrays, and strings are invalid.
131
+ #
132
+ # @param [Hash] options (<code>{}</code>) Additional options for
133
+ # storing the item.
134
+ #
135
+ # @option options [Hash] :if Designates a conditional put. The
136
+ # operation will fail unless the item exists and has the
137
+ # attributes in the value for this option. For example:
138
+ #
139
+ # # throws DynamoDB::Errors::ConditionalCheckFailedException
140
+ # # unless the item has "color" set to "red"
141
+ # items.put(
142
+ # { :foo => "Bar" },
143
+ # :if => { :color => "red" }
144
+ # )
145
+ #
146
+ # @option options [String, Symbol, Array] :unless_exists A name
147
+ # or collection of attribute names; if the item already exists
148
+ # and has a value for any of these attributes, this method
149
+ # will raise
150
+ # +DynamoDB::Errors::ConditionalCheckFailedException+. For example:
151
+ #
152
+ # items.put({ :id => "abc123" }, :unless_exists => "id")
153
+ #
154
+ # @option options [Symbol] :return If set to +:all_old+, this
155
+ # method will return a hash containing the previous values of
156
+ # all attributes for the item that was overwritten. If this
157
+ # option is set to +:none+, or if it is set to +:all_old+ and
158
+ # no item currently exists with the same primary key values,
159
+ # the method will return +nil+.
160
+ #
161
+ # @return [Item] An object representing the item that was
162
+ # stored. Note that the SDK retains only the item's primary
163
+ # key values in memory; if you access the attributes of the
164
+ # item using the returned object, the SDK will contact the
165
+ # service to retrieve those attributes. The +:return+ option
166
+ # may be used to change the return value of this method.
167
+ def create attributes, options = {}
168
+ table.assert_schema!
169
+
170
+ attributes = attributes.inject({}) do |hash, (key, value)|
171
+ context = "value for attribute #{key}"
172
+ hash.update(key.to_s => format_attribute_value(value, context))
173
+ end
174
+
175
+ client_opts = {
176
+ :table_name => table.name,
177
+ :item => attributes
178
+ }
179
+
180
+ expected = expect_conditions(options)
181
+ client_opts[:expected] = expected unless expected.empty?
182
+
183
+ client_opts[:return_values] = options[:return].to_s.upcase if
184
+ options[:return]
185
+
186
+ resp = client.put_item(client_opts)
187
+
188
+ item = Item.new_from(:put_item, attributes, table)
189
+
190
+ if options[:return]
191
+ values_from_response_hash(resp.data["Attributes"])
192
+ else
193
+ item
194
+ end
195
+ end
196
+ alias_method :put, :create
197
+
198
+ # Returns an object representing an item in the table,
199
+ # identified by its hash key value. This method will raise an
200
+ # exception unless the table has a schema loaded or configured,
201
+ # or if the table has a composite primary key.
202
+ #
203
+ # table.hash_key = [:id, :string]
204
+ # item = table.items["abc123"]
205
+ #
206
+ # @param [String, Numeric] hash_value The hash key value for the
207
+ # item. The type of this parameter must match the type in the
208
+ # table's schema, but currently the SDK makes no attempt to
209
+ # validate the key.
210
+ #
211
+ # @return [Item]
212
+ def [] hash_value
213
+ table.assert_schema!
214
+ raise(ArgumentError,
215
+ "table has a range key; use #at instead of #[]") unless
216
+ table.simple_key?
217
+ Item.new(table, hash_value)
218
+ end
219
+
220
+ # Returns an object representing an item in the table,
221
+ # identified by its hash key value and conditionally its range
222
+ # key value. This method will raise an exception unless the
223
+ # table has a schema loaded or configured. The type of each
224
+ # parameter must match the type in the table's schema, but
225
+ # currently the SDK makes no attempt to validate the key
226
+ # elements.
227
+ #
228
+ # table.hash_key = [:id, :string]
229
+ # table.range_key = [:range, :number]
230
+ # item = table.items.at("abc123", 12)
231
+ #
232
+ # @param [String, Numeric] hash_value The hash key value for the
233
+ # item.
234
+ #
235
+ # @param [String, Numeric] range_value The range key value for
236
+ # the item. This parameter is required when the table has a
237
+ # composite primary key, and it may not be specified when the
238
+ # table has a simple primary key.
239
+ #
240
+ # @return [Item]
241
+ def at hash_value, range_value = nil
242
+ table.assert_schema!
243
+ if table.composite_key? and !range_value
244
+ raise ArgumentError, "a range key value is required for this table"
245
+ end
246
+ Item.new(table, hash_value, range_value)
247
+ end
248
+ alias_method :[], :at
249
+
250
+ # Provides a convenient syntax for expressing scan filters.
251
+ #
252
+ # table.items.where(:path).begins_with("users/")
253
+ #
254
+ class FilterBuilder
255
+
256
+ include Types
257
+
258
+ attr_reader :items
259
+
260
+ attr_reader :attribute
261
+
262
+ # @private
263
+ def initialize(items, attribute)
264
+ @items = items
265
+ @attribute = attribute
266
+ end
267
+
268
+ # Filters the collection to include only those items where the
269
+ # value of this attribute is equal to the argument.
270
+ #
271
+ # @param [String, Numeric] value The value to compare against.
272
+ #
273
+ # @return [ItemCollection] A new item collection filtered by
274
+ # this condition.
275
+ def equals value
276
+ @items.with_filter(attribute, "EQ", value)
277
+ end
278
+
279
+ # Filters the collection to include only those items where the
280
+ # value of this attribute is not equal to the argument.
281
+ #
282
+ # @param [String, Numeric] value The value to compare against.
283
+ #
284
+ # @return [ItemCollection] A new item collection filtered by
285
+ # this condition.
286
+ def not_equal_to value
287
+ @items.with_filter(attribute, "NE", value)
288
+ end
289
+
290
+ # Filters the collection to include only those items where the
291
+ # value of this attribute is less than the argument.
292
+ #
293
+ # @param [String, Numeric] value The value to compare against.
294
+ #
295
+ # @return [ItemCollection] A new item collection filtered by
296
+ # this condition.
297
+ def less_than value
298
+ @items.with_filter(attribute, "LT", value)
299
+ end
300
+
301
+ # Filters the collection to include only those items where the
302
+ # value of this attribute is greater than the argument.
303
+ #
304
+ # @param [String, Numeric] value The value to compare against.
305
+ #
306
+ # @return [ItemCollection] A new item collection filtered by
307
+ # this condition.
308
+ def greater_than value
309
+ @items.with_filter(attribute, "GT", value)
310
+ end
311
+
312
+ # Filters the collection to include only those items where the
313
+ # value of this attribute is less than or equal to the
314
+ # argument.
315
+ #
316
+ # @param [String, Numeric] value The value to compare against.
317
+ #
318
+ # @return [ItemCollection] A new item collection filtered by
319
+ # this condition.
320
+ def lte value
321
+ @items.with_filter(attribute, "LE", value)
322
+ end
323
+
324
+ # Filters the collection to include only those items where the
325
+ # value of this attribute is greater than or equal to the
326
+ # argument.
327
+ #
328
+ # @param [String, Numeric] value The value to compare against.
329
+ #
330
+ # @return [ItemCollection] A new item collection filtered by
331
+ # this condition.
332
+ def gte value
333
+ @items.with_filter(attribute, "GE", value)
334
+ end
335
+
336
+ # Filters the collection to include only those items where
337
+ # this attribute does not exist.
338
+ #
339
+ # @return [ItemCollection] A new item collection filtered by
340
+ # this condition.
341
+ def is_null
342
+ @items.with_filter(attribute, "NULL")
343
+ end
344
+
345
+ # Filters the collection to include only those items where
346
+ # this attribute exists.
347
+ #
348
+ # @return [ItemCollection] A new item collection filtered by
349
+ # this condition.
350
+ def not_null
351
+ @items.with_filter(attribute, "NOT_NULL")
352
+ end
353
+
354
+ # Filters the collection to include only those items where
355
+ # this attribute contains the argument. If the attribute
356
+ # value is a set, this filter matches items where the argument
357
+ # is one of the values in the set. If the attribute value is
358
+ # a string, this filter matches items where the argument
359
+ # (which must be a string) is a substring of the attribute
360
+ # value.
361
+ #
362
+ # @param [String, Numeric] value The value to compare against.
363
+ #
364
+ # @return [ItemCollection] A new item collection filtered by
365
+ # this condition.
366
+ def contains value
367
+ @items.with_filter(attribute, "CONTAINS", value)
368
+ end
369
+
370
+ # Filters the collection to include only those items where
371
+ # this attribute does not contain the argument. If the
372
+ # attribute value is a set, this filter matches items where
373
+ # the argument is not present in the set. If the attribute
374
+ # value is a string, this filter matches items where the
375
+ # argument (which must be a string) is not a substring of the
376
+ # attribute value.
377
+ #
378
+ # @param [String, Numeric] value The value to compare against.
379
+ #
380
+ # @return [ItemCollection] A new item collection filtered by
381
+ # this condition.
382
+ def does_not_contain value
383
+ @items.with_filter(attribute, "NOT_CONTAINS", value)
384
+ end
385
+
386
+ # Filters the collection to include only those items where
387
+ # this attribute begins with the argument.
388
+ #
389
+ # @param [String] value The value to compare against.
390
+ #
391
+ # @return [ItemCollection] A new item collection filtered by
392
+ # this condition.
393
+ def begins_with value
394
+ @items.with_filter(attribute, "BEGINS_WITH", value)
395
+ end
396
+
397
+ # Filters the collection to include only those items where
398
+ # this attribute equals one of the arguments.
399
+ #
400
+ # @param [Array<String, Numeric>] values The values to compare
401
+ # against.
402
+ #
403
+ # @return [ItemCollection] A new item collection filtered by
404
+ # this condition.
405
+ def in *values
406
+ @items.with_filter(attribute, "IN", *values)
407
+ end
408
+
409
+ # Filters the collection to include only those items where
410
+ # this attribute is between the two arguments.
411
+ #
412
+ # @param [String, Numeric] min The minimum value.
413
+ #
414
+ # @param [String, Numeric] max The maximum value.
415
+ #
416
+ # @return [ItemCollection] A new item collection filtered by
417
+ # this condition.
418
+ def between min, max
419
+ @items.with_filter(attribute, "BETWEEN", min, max)
420
+ end
421
+
422
+ end
423
+
424
+ # @overload where(attributes)
425
+ #
426
+ # table.items.where(:name => "Fred")
427
+ #
428
+ # @param [Hash] attributes The returned collection will be
429
+ # filtered such that each item contains the attributes and
430
+ # values in this map.
431
+ #
432
+ # @return [ItemCollection] A collection where all the items
433
+ # have the provided attributes and values.
434
+ #
435
+ # @overload where(attribute_name)
436
+ #
437
+ # table.items.where(:name).equals("Fred")
438
+ #
439
+ # @return [FilterBuilder] An object that allows you to specify
440
+ # a filter on the provided attribute name.
441
+ def where(filter)
442
+ case filter
443
+ when Hash
444
+ filter.inject(self) do |items, (name, value)|
445
+ case value
446
+ when nil
447
+ items.with_filter(name.to_s, "NULL")
448
+ when Range
449
+ items.with_filter(name.to_s, "BETWEEN", value.begin, value.end)
450
+ else
451
+ items.with_filter(name.to_s, "EQ", value)
452
+ end
453
+ end
454
+ when String, Symbol
455
+ FilterBuilder.new(self, filter.to_s)
456
+ end
457
+ end
458
+ alias_method :and, :where
459
+
460
+ # Iterates over all the items in the collection using a scan
461
+ # operation. A scan operation scans the entire table. You can
462
+ # specify filters to apply to the results to refine the values
463
+ # returned to you, after the complete scan. Amazon DynamoDB puts
464
+ # a 1MB limit on the scan (the limit applies before the results
465
+ # are filtered). A scan can result in no table data meeting the
466
+ # filter criteria.
467
+ #
468
+ # For more information about filtering the collection or
469
+ # limiting the results that are returned, see the {#where} and
470
+ # {#limit} methods.
471
+ #
472
+ # @param [Hash] options Options for iterating the collection.
473
+ #
474
+ # @yieldparam [Item] item Each item in the collection.
475
+ #
476
+ # @option options [Integer] :limit The maximum number of items to yield.
477
+ #
478
+ # @option options [Integer] :batch_size The maximum number of items
479
+ # to retrieve with each service request.
480
+ def each(options = {}, &block)
481
+
482
+ if conditions = options.delete(:where)
483
+ return where(conditions).each(options, &block)
484
+ end
485
+
486
+ table.assert_schema!
487
+
488
+ options = options.merge(:table_name => table.name)
489
+ options[:scan_filter] = scan_filters unless scan_filters.empty?
490
+
491
+ unless options[:count] or options[:item_data]
492
+ options[:attributes_to_get] = [table.hash_key.name]
493
+ options[:attributes_to_get] << table.range_key.name if
494
+ table.composite_key?
495
+ end
496
+
497
+ super(options, &block)
498
+ end
499
+
500
+ # Retrieves data about the items in the collection. This method
501
+ # works like {#each}, except that it returns or yields
502
+ # {ItemData} instances instead of {Item} instances. This is
503
+ # useful if you want to use the attributes of the item in a loop
504
+ # or retain them in memory. Also, unlike {#each} which always
505
+ # requests only the primary key attributes of the items, this
506
+ # method allows you to specify which attributes to retrieve from
507
+ # DynamoDB.
508
+ #
509
+ # # fetch all attributes for a collection of items
510
+ # items.select { |data| p data.attributes }
511
+ #
512
+ # # fetch only the "color" attribute of each item
513
+ # items.select(:color) { |data| p data.attributes["color"] }
514
+ #
515
+ # # use client-side filtering to delete a subset of the items
516
+ # items.select do |data|
517
+ # data.item.delete if data.attributes.size % 2 == 0
518
+ # end
519
+ #
520
+ # @param [Array<String, Symbol>] attributes Specifies which
521
+ # attributes to retrieve from the service. By default all
522
+ # attributes are retrieved. If the last argument is a hash,
523
+ # it may contain options for iterating the items in the
524
+ # collection. See the {#each} method for more information
525
+ # about these options.
526
+ #
527
+ # @param [Hash] options
528
+ #
529
+ # @option [Integer] :limit The maximum number of records to
530
+ # select (scan). If more records are requested than can
531
+ # be returned in a single response, multiple requests
532
+ # will be made.
533
+ #
534
+ # @yieldparam [ItemData] data The data for each item in the
535
+ # collection. The attributes of each item will be populated
536
+ # in the ItemData object; however, {ItemData#item} will not be
537
+ # populated unless the requested attributes include all
538
+ # elements of the table's primary key. For example, if a
539
+ # table has a composite primary key, this method will only
540
+ # populate {ItemData#item} if the list of requested attributes
541
+ # includes both the hash key and range key attributes.
542
+ #
543
+ # @return [Enumerator, nil] If a block is given, this method
544
+ # returns nil. Otherwise, it returns an enumerator for the
545
+ # values that would have been yielded to the block.
546
+ #
547
+ def select *attributes, &block
548
+
549
+ options = {}
550
+ options = attributes.pop if attributes.last.kind_of?(Hash)
551
+ options = options.merge(:item_data => true)
552
+ options[:attributes_to_get] =
553
+ attributes.map { |att| att.to_s } unless
554
+ attributes.empty?
555
+
556
+ if block_given?
557
+ each(options, &block)
558
+ else
559
+ enumerator(options)
560
+ end
561
+
562
+ end
563
+
564
+ # Counts the items in the collection using a table scan. The
565
+ # count applies to the items that match all the filters on the
566
+ # collection. For example:
567
+ #
568
+ # # count the blue items
569
+ # items.where(:color => "blue").count
570
+ #
571
+ # @param [Hash] options Options for counting the items.
572
+ #
573
+ # @option options [Integer] :max_requests The maximum number of
574
+ # requests to make.
575
+ #
576
+ # @option options [Integer] :limit The maximum count; the return
577
+ # value will be less than or equal to the value of this
578
+ # option.
579
+ #
580
+ # @option options [Integer] :batch_size DynamoDB will scan up to
581
+ # 1MB of data on each request; you can use this option to
582
+ # further limit the number of items scanned on each request.
583
+ #
584
+ # @return [Integer]
585
+ def count options = {}
586
+ options = options.merge(:count => true)
587
+
588
+ # since each with :count yields the per-page counts, each with
589
+ # :limit and :count effectively limits the number of requests,
590
+ # not the number of items
591
+ limit = options.delete(:limit)
592
+ options[:limit] = options.delete(:max_requests) if
593
+ options.key?(:max_requests)
594
+
595
+ # it usually doesn't make sense to ask for more items than you
596
+ # care about counting
597
+ options[:batch_size] ||= limit if limit
598
+
599
+ enumerator(options).inject(0) do |sum, n|
600
+ return limit if limit && sum + n >= limit
601
+ sum + n
602
+ end
603
+ end
604
+
605
+ RANGE_KEY_OPTIONS = {
606
+ :range_less_than => "LT",
607
+ :range_greater_than => "GT",
608
+ :range_lte => "LE",
609
+ :range_gte => "GE",
610
+ :range_begins_with => "BEGINS_WITH"
611
+ }
612
+
613
+ # Queries the items in the table by primary key values. This
614
+ # operation is generally more efficient than the scan operation,
615
+ # which always scans the whole table. A Query operation
616
+ # searches for a specific range of keys satisfying a given set
617
+ # of key conditions and does not have the added step of
618
+ # filtering out results.
619
+ #
620
+ # # find all items with a given hash key value
621
+ # items.query(:hash_value => "abc123")
622
+ #
623
+ # # get only the colors attribute of each item
624
+ # items.query(
625
+ # :hash_value => "abc123",
626
+ # :select => :colors
627
+ # )
628
+ #
629
+ # # find only the items where the range key is between two values
630
+ # items.query(
631
+ # :hash_value => "abc123",
632
+ # :range_value => 1..100
633
+ # )
634
+ #
635
+ # @note This method is only valid for tables with a composite
636
+ # primary key.
637
+ #
638
+ # @param [Hash] options Options for the query. +:hash_value+ is
639
+ # required. Only one of the following options may be set:
640
+ #
641
+ # * +:range_value+
642
+ # * +:range_greater_than+
643
+ # * +:range_less_than+
644
+ # * +:range_gte+
645
+ # * +:range_lte+
646
+ # * +:range_begins_with+
647
+ #
648
+ # @option options [String, Numeric] :hash_value Attribute value
649
+ # of the hash component of the composite primary key.
650
+ #
651
+ # @option options [Array<String, Symbol>, String, Symbol] :select
652
+ # Attribute name or names to retrieve. When this option is
653
+ # set, the returned or yielded items will be instances of
654
+ # {ItemData} instead of {Item}. The special value +:all+
655
+ # indicates that all attributes should be retrieved and
656
+ # returned in ItemData instances.
657
+ #
658
+ # @option options [String, Numeric, Range] :range_value
659
+ # Specifies which range key values to find in the table. If
660
+ # this is a Range, the query will return items with range key
661
+ # values between the beginning and end of the range
662
+ # (inclusive). If it is a string or number, the query will
663
+ # return only the item with that range key value.
664
+ #
665
+ # @option options [String, Numeric] :range_greater_than Matches
666
+ # items where the range key value is greater than this value.
667
+ #
668
+ # @option options [String, Numeric] :range_less_than Matches
669
+ # items where the range key value is less than this value.
670
+ #
671
+ # @option options [String, Numeric] :range_gte Matches items
672
+ # where the range key value is greater than or equal to this
673
+ # value.
674
+ #
675
+ # @option options [String, Numeric] :range_lte Matches items
676
+ # where the range key value is less than or equal to this
677
+ # value.
678
+ #
679
+ # @option options [String, Numeric] :range_begins_with Matches
680
+ # items where the range key value begins with this value.
681
+ # This option is only valid if the range key is a string.
682
+ def query(options = {}, &block)
683
+
684
+ options = options.merge(:query => true)
685
+
686
+ raise ArgumentError, "a hash key value is required" unless
687
+ options[:hash_value]
688
+
689
+ options[:hash_key_value] =
690
+ format_attribute_value(options.delete(:hash_value))
691
+
692
+ range = options.delete(:range_value)
693
+ range_op = nil
694
+ value_list = []
695
+ if range and range.kind_of?(Range)
696
+ value_list = [format_attribute_value(range.begin),
697
+ format_attribute_value(range.end)]
698
+ range_op = "BETWEEN"
699
+ elsif range
700
+ value_list = [format_attribute_value(range)]
701
+ range_op = "EQ"
702
+ end
703
+
704
+ RANGE_KEY_OPTIONS.each do |name, op|
705
+ if value = options.delete(name)
706
+ raise(ArgumentError,
707
+ "only one range key condition is supported") if range_op
708
+ range_op = op
709
+ value_list = [format_attribute_value(value)]
710
+ end
711
+ end
712
+
713
+ options[:range_key_condition] = {
714
+ :attribute_value_list => value_list,
715
+ :comparison_operator => range_op
716
+ } if range_op
717
+
718
+ if select = options.delete(:select)
719
+ options[:item_data] = true
720
+ options[:attributes_to_get] = select.map do |att|
721
+ att.to_s
722
+ end unless select == :all
723
+ end
724
+
725
+ if block
726
+ each(options, &block)
727
+ else
728
+ enumerator(options)
729
+ end
730
+ end
731
+
732
+ # @private
733
+ def with_filter attribute, op, *values
734
+
735
+ values = values.map {|value| format_attribute_value(value) }
736
+
737
+ filter = {
738
+ :attribute_value_list => values,
739
+ :comparison_operator => op
740
+ }
741
+
742
+ if scan_filters.key?(attribute)
743
+ raise(ArgumentError, "conflicting filters for attribute #{attribute}")
744
+ end
745
+
746
+ refine(:scan_filters => scan_filters.merge(attribute => filter))
747
+
748
+ end
749
+
750
+ # @private
751
+ def refine(opts)
752
+ opts = {
753
+ :scan_filters => scan_filters
754
+ }.merge(opts)
755
+ self.class.new(table, opts)
756
+ end
757
+
758
+ protected
759
+ def _each_item next_token, limit, options = {}, &block
760
+
761
+ options[:exclusive_start_key] = next_token if next_token
762
+
763
+ options[:limit] = limit if limit
764
+
765
+ method = options.delete(:query) ? :query : :scan
766
+
767
+ mode = case
768
+ when options.delete(:item_data) then :item_data
769
+ when options[:count] then :count
770
+ else :item
771
+ end
772
+
773
+ response = client.send(method, options)
774
+
775
+ _yield_items(mode, response, &block)
776
+
777
+ response.data["LastEvaluatedKey"]
778
+
779
+ end
780
+
781
+ protected
782
+ def _yield_items mode, response, &block
783
+
784
+ case mode
785
+
786
+ # yield the count of items matching
787
+ when :count
788
+ yield(response.data["Count"])
789
+
790
+ # yeild item data objects
791
+ when :item_data
792
+
793
+ table.assert_schema!
794
+
795
+ #construct_items =
796
+ # (true if request_includes_key?(response.request_options))
797
+
798
+ construct_items = request_includes_key?(response.request_options)
799
+
800
+ response.data["Items"].each do |i|
801
+ attributes = values_from_response_hash(i)
802
+
803
+ item = nil
804
+ item = Item.new_from(:put_item, i, table) if construct_items
805
+
806
+ item_data = ItemData.new(:item => item, :attributes => attributes)
807
+
808
+ yield(item_data)
809
+
810
+ end
811
+
812
+ # yield item objects
813
+ when :item
814
+ response.data["Items"].each do |i|
815
+ item = Item.new_from(:put_item, i, table)
816
+ yield(item)
817
+ end
818
+
819
+ end
820
+
821
+ end
822
+
823
+ protected
824
+ def request_includes_key?(options)
825
+ requested_atts = options[:attributes_to_get]
826
+ requested_atts.nil? or
827
+ (table.simple_key? &&
828
+ requested_atts.include?(table.hash_key.name)) or
829
+ (table.composite_key? &&
830
+ requested_atts.include?(table.hash_key.name) &&
831
+ requested_atts.include?(table.range_key.name))
832
+ end
833
+
834
+ end
835
+
836
+ end
837
+ end