aws-sdk 1.2.6 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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