dynamoid-moda 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. checksums.yaml +15 -0
  2. data/.document +5 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +7 -0
  5. data/Gemfile +24 -0
  6. data/Gemfile.lock +118 -0
  7. data/Gemfile_activemodel4 +24 -0
  8. data/Gemfile_activemodel4.lock +88 -0
  9. data/LICENSE.txt +20 -0
  10. data/README.markdown +360 -0
  11. data/Rakefile +93 -0
  12. data/VERSION +1 -0
  13. data/doc/.nojekyll +0 -0
  14. data/doc/Dynamoid.html +328 -0
  15. data/doc/Dynamoid/Adapter.html +1872 -0
  16. data/doc/Dynamoid/Adapter/AwsSdk.html +2101 -0
  17. data/doc/Dynamoid/Adapter/Local.html +1574 -0
  18. data/doc/Dynamoid/Associations.html +138 -0
  19. data/doc/Dynamoid/Associations/Association.html +847 -0
  20. data/doc/Dynamoid/Associations/BelongsTo.html +161 -0
  21. data/doc/Dynamoid/Associations/ClassMethods.html +766 -0
  22. data/doc/Dynamoid/Associations/HasAndBelongsToMany.html +167 -0
  23. data/doc/Dynamoid/Associations/HasMany.html +167 -0
  24. data/doc/Dynamoid/Associations/HasOne.html +161 -0
  25. data/doc/Dynamoid/Associations/ManyAssociation.html +1684 -0
  26. data/doc/Dynamoid/Associations/SingleAssociation.html +627 -0
  27. data/doc/Dynamoid/Components.html +242 -0
  28. data/doc/Dynamoid/Config.html +412 -0
  29. data/doc/Dynamoid/Config/Options.html +638 -0
  30. data/doc/Dynamoid/Criteria.html +138 -0
  31. data/doc/Dynamoid/Criteria/Chain.html +1471 -0
  32. data/doc/Dynamoid/Criteria/ClassMethods.html +105 -0
  33. data/doc/Dynamoid/Dirty.html +424 -0
  34. data/doc/Dynamoid/Dirty/ClassMethods.html +174 -0
  35. data/doc/Dynamoid/Document.html +1033 -0
  36. data/doc/Dynamoid/Document/ClassMethods.html +1116 -0
  37. data/doc/Dynamoid/Errors.html +125 -0
  38. data/doc/Dynamoid/Errors/ConditionalCheckFailedException.html +141 -0
  39. data/doc/Dynamoid/Errors/DocumentNotValid.html +221 -0
  40. data/doc/Dynamoid/Errors/Error.html +137 -0
  41. data/doc/Dynamoid/Errors/InvalidField.html +141 -0
  42. data/doc/Dynamoid/Errors/InvalidQuery.html +131 -0
  43. data/doc/Dynamoid/Errors/MissingRangeKey.html +141 -0
  44. data/doc/Dynamoid/Fields.html +686 -0
  45. data/doc/Dynamoid/Fields/ClassMethods.html +438 -0
  46. data/doc/Dynamoid/Finders.html +135 -0
  47. data/doc/Dynamoid/Finders/ClassMethods.html +943 -0
  48. data/doc/Dynamoid/IdentityMap.html +492 -0
  49. data/doc/Dynamoid/IdentityMap/ClassMethods.html +534 -0
  50. data/doc/Dynamoid/Indexes.html +321 -0
  51. data/doc/Dynamoid/Indexes/ClassMethods.html +369 -0
  52. data/doc/Dynamoid/Indexes/Index.html +1142 -0
  53. data/doc/Dynamoid/Middleware.html +115 -0
  54. data/doc/Dynamoid/Middleware/IdentityMap.html +264 -0
  55. data/doc/Dynamoid/Persistence.html +892 -0
  56. data/doc/Dynamoid/Persistence/ClassMethods.html +836 -0
  57. data/doc/Dynamoid/Validations.html +415 -0
  58. data/doc/_index.html +506 -0
  59. data/doc/class_list.html +53 -0
  60. data/doc/css/common.css +1 -0
  61. data/doc/css/full_list.css +57 -0
  62. data/doc/css/style.css +338 -0
  63. data/doc/file.LICENSE.html +73 -0
  64. data/doc/file.README.html +416 -0
  65. data/doc/file_list.html +58 -0
  66. data/doc/frames.html +28 -0
  67. data/doc/index.html +416 -0
  68. data/doc/js/app.js +214 -0
  69. data/doc/js/full_list.js +178 -0
  70. data/doc/js/jquery.js +4 -0
  71. data/doc/method_list.html +1144 -0
  72. data/doc/top-level-namespace.html +112 -0
  73. data/dynamoid-moda.gemspec +210 -0
  74. data/dynamoid.gemspec +208 -0
  75. data/lib/dynamoid.rb +46 -0
  76. data/lib/dynamoid/adapter.rb +267 -0
  77. data/lib/dynamoid/adapter/aws_sdk.rb +309 -0
  78. data/lib/dynamoid/associations.rb +106 -0
  79. data/lib/dynamoid/associations/association.rb +105 -0
  80. data/lib/dynamoid/associations/belongs_to.rb +44 -0
  81. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +40 -0
  82. data/lib/dynamoid/associations/has_many.rb +39 -0
  83. data/lib/dynamoid/associations/has_one.rb +39 -0
  84. data/lib/dynamoid/associations/many_association.rb +191 -0
  85. data/lib/dynamoid/associations/single_association.rb +69 -0
  86. data/lib/dynamoid/components.rb +37 -0
  87. data/lib/dynamoid/config.rb +57 -0
  88. data/lib/dynamoid/config/options.rb +78 -0
  89. data/lib/dynamoid/criteria.rb +29 -0
  90. data/lib/dynamoid/criteria/chain.rb +326 -0
  91. data/lib/dynamoid/dirty.rb +47 -0
  92. data/lib/dynamoid/document.rb +199 -0
  93. data/lib/dynamoid/errors.rb +28 -0
  94. data/lib/dynamoid/fields.rb +138 -0
  95. data/lib/dynamoid/finders.rb +133 -0
  96. data/lib/dynamoid/identity_map.rb +96 -0
  97. data/lib/dynamoid/indexes.rb +69 -0
  98. data/lib/dynamoid/indexes/index.rb +103 -0
  99. data/lib/dynamoid/middleware/identity_map.rb +16 -0
  100. data/lib/dynamoid/persistence.rb +292 -0
  101. data/lib/dynamoid/validations.rb +36 -0
  102. data/spec/app/models/address.rb +13 -0
  103. data/spec/app/models/camel_case.rb +34 -0
  104. data/spec/app/models/car.rb +6 -0
  105. data/spec/app/models/magazine.rb +11 -0
  106. data/spec/app/models/message.rb +9 -0
  107. data/spec/app/models/nuclear_submarine.rb +5 -0
  108. data/spec/app/models/sponsor.rb +8 -0
  109. data/spec/app/models/subscription.rb +12 -0
  110. data/spec/app/models/tweet.rb +12 -0
  111. data/spec/app/models/user.rb +26 -0
  112. data/spec/app/models/vehicle.rb +7 -0
  113. data/spec/dynamoid/adapter/aws_sdk_spec.rb +376 -0
  114. data/spec/dynamoid/adapter_spec.rb +155 -0
  115. data/spec/dynamoid/associations/association_spec.rb +194 -0
  116. data/spec/dynamoid/associations/belongs_to_spec.rb +71 -0
  117. data/spec/dynamoid/associations/has_and_belongs_to_many_spec.rb +47 -0
  118. data/spec/dynamoid/associations/has_many_spec.rb +42 -0
  119. data/spec/dynamoid/associations/has_one_spec.rb +45 -0
  120. data/spec/dynamoid/associations_spec.rb +16 -0
  121. data/spec/dynamoid/config_spec.rb +27 -0
  122. data/spec/dynamoid/criteria/chain_spec.rb +210 -0
  123. data/spec/dynamoid/criteria_spec.rb +75 -0
  124. data/spec/dynamoid/dirty_spec.rb +57 -0
  125. data/spec/dynamoid/document_spec.rb +180 -0
  126. data/spec/dynamoid/fields_spec.rb +156 -0
  127. data/spec/dynamoid/finders_spec.rb +147 -0
  128. data/spec/dynamoid/identity_map_spec.rb +45 -0
  129. data/spec/dynamoid/indexes/index_spec.rb +104 -0
  130. data/spec/dynamoid/indexes_spec.rb +25 -0
  131. data/spec/dynamoid/persistence_spec.rb +301 -0
  132. data/spec/dynamoid/validations_spec.rb +36 -0
  133. data/spec/dynamoid_spec.rb +14 -0
  134. data/spec/spec_helper.rb +55 -0
  135. data/spec/support/with_partitioning.rb +15 -0
  136. metadata +363 -0
data/lib/dynamoid.rb ADDED
@@ -0,0 +1,46 @@
1
+ require "delegate"
2
+ require "time"
3
+ require "securerandom"
4
+ require "active_support/core_ext"
5
+ require 'active_support/json'
6
+ require "active_support/inflector"
7
+ require "active_support/lazy_load_hooks"
8
+ require "active_support/time_with_zone"
9
+ require "active_model"
10
+
11
+ require 'dynamoid/errors'
12
+ require 'dynamoid/fields'
13
+ require 'dynamoid/indexes'
14
+ require 'dynamoid/associations'
15
+ require 'dynamoid/persistence'
16
+ require 'dynamoid/dirty'
17
+ require 'dynamoid/validations'
18
+ require 'dynamoid/criteria'
19
+ require 'dynamoid/finders'
20
+ require 'dynamoid/identity_map'
21
+ require 'dynamoid/config'
22
+ require 'dynamoid/components'
23
+ require 'dynamoid/document'
24
+ require 'dynamoid/adapter'
25
+
26
+ require 'dynamoid/middleware/identity_map'
27
+
28
+ module Dynamoid
29
+ extend self
30
+
31
+ MAX_ITEM_SIZE = 65_536
32
+
33
+ def configure
34
+ block_given? ? yield(Dynamoid::Config) : Dynamoid::Config
35
+ end
36
+ alias :config :configure
37
+
38
+ def logger
39
+ Dynamoid::Config.logger
40
+ end
41
+
42
+ def included_models
43
+ @included_models ||= []
44
+ end
45
+
46
+ end
@@ -0,0 +1,267 @@
1
+ # encoding: utf-8
2
+ module Dynamoid
3
+
4
+ # Adapter provides a generic, write-through class that abstracts variations in the underlying connections to provide a uniform response
5
+ # to Dynamoid.
6
+ module Adapter
7
+ extend self
8
+ attr_accessor :tables
9
+
10
+ # The actual adapter currently in use: presently AwsSdk.
11
+ #
12
+ # @since 0.2.0
13
+ def adapter
14
+ reconnect! unless @adapter
15
+ @adapter
16
+ end
17
+
18
+ # Establishes a connection to the underyling adapter and caches all its tables for speedier future lookups. Issued when the adapter is first called.
19
+ #
20
+ # @since 0.2.0
21
+ def reconnect!
22
+ require "dynamoid/adapter/#{Dynamoid::Config.adapter}" unless Dynamoid::Adapter.const_defined?(Dynamoid::Config.adapter.camelcase)
23
+ @adapter = Dynamoid::Adapter.const_get(Dynamoid::Config.adapter.camelcase)
24
+ @adapter.connect! if @adapter.respond_to?(:connect!)
25
+ self.tables = benchmark('Cache Tables') {list_tables}
26
+ end
27
+
28
+ # Shows how long it takes a method to run on the adapter. Useful for generating logged output.
29
+ #
30
+ # @param [Symbol] method the name of the method to appear in the log
31
+ # @param [Array] args the arguments to the method to appear in the log
32
+ # @yield the actual code to benchmark
33
+ #
34
+ # @return the result of the yield
35
+ #
36
+ # @since 0.2.0
37
+ def benchmark(method, *args)
38
+ start = Time.now
39
+ result = yield
40
+ Dynamoid.logger.info "(#{((Time.now - start) * 1000.0).round(2)} ms) #{method.to_s.split('_').collect(&:upcase).join(' ')}#{ " - #{args.inspect}" unless args.nil? || args.empty? }"
41
+ return result
42
+ end
43
+
44
+ # Write an object to the adapter. Partition it to a randomly selected key first if necessary.
45
+ #
46
+ # @param [String] table the name of the table to write the object to
47
+ # @param [Object] object the object itself
48
+ # @param [Hash] options Options that are passed to the put_item call
49
+ #
50
+ # @return [Object] the persisted object
51
+ #
52
+ # @since 0.2.0
53
+ def write(table, object, options = nil)
54
+ if Dynamoid::Config.partitioning? && object[:id]
55
+ object[:id] = "#{object[:id]}.#{Random.rand(Dynamoid::Config.partition_size)}"
56
+ object[:updated_at] = Time.now.to_f
57
+ end
58
+ put_item(table, object, options)
59
+ end
60
+
61
+ # Read one or many keys from the selected table. This method intelligently calls batch_get or get on the underlying adapter depending on
62
+ # whether ids is a range or a single key: additionally, if partitioning is enabled, it batch_gets all keys in the partition space
63
+ # automatically. Finally, if a range key is present, it will also interpolate that into the ids so that the batch get will acquire the
64
+ # correct record.
65
+ #
66
+ # @param [String] table the name of the table to write the object to
67
+ # @param [Array] ids to fetch, can also be a string of just one id
68
+ # @param [Hash] options: Passed to the underlying query. The :range_key option is required whenever the table has a range key,
69
+ # unless multiple ids are passed in and Dynamoid::Config.partitioning? is turned off.
70
+ #
71
+ # @since 0.2.0
72
+ def read(table, ids, options = {})
73
+ range_key = options.delete(:range_key)
74
+
75
+ if ids.respond_to?(:each)
76
+ ids = ids.collect{|id| range_key ? [id, range_key] : id}
77
+ if Dynamoid::Config.partitioning?
78
+ results = batch_get_item({table => id_with_partitions(ids)}, options)
79
+ {table => result_for_partition(results[table],table)}
80
+ else
81
+ batch_get_item({table => ids}, options)
82
+ end
83
+ else
84
+ if Dynamoid::Config.partitioning?
85
+ ids = range_key ? [[ids, range_key]] : ids
86
+ results = batch_get_item({table => id_with_partitions(ids)}, options)
87
+ result_for_partition(results[table],table).first
88
+ else
89
+ options[:range_key] = range_key if range_key
90
+ get_item(table, ids, options)
91
+ end
92
+ end
93
+ end
94
+
95
+ # Delete an item from a table. If partitioning is turned on, deletes all partitioned keys as well.
96
+ #
97
+ # @param [String] table the name of the table to write the object to
98
+ # @param [Array] ids to delete, can also be a string of just one id
99
+ # @param [Array] range_key of the record to delete, can also be a string of just one range_key
100
+ #
101
+ def delete(table, ids, options = {})
102
+ range_key = options[:range_key] #array of range keys that matches the ids passed in
103
+ if ids.respond_to?(:each)
104
+ if range_key.respond_to?(:each)
105
+ #turn ids into array of arrays each element being hash_key, range_key
106
+ ids = ids.each_with_index.map{|id,i| [id,range_key[i]]}
107
+ else
108
+ ids = range_key ? [[ids, range_key]] : ids
109
+ end
110
+
111
+ if Dynamoid::Config.partitioning?
112
+ batch_delete_item(table => id_with_partitions(ids))
113
+ else
114
+ batch_delete_item(table => ids)
115
+ end
116
+ else
117
+ if Dynamoid::Config.partitioning?
118
+ ids = range_key ? [[ids, range_key]] : ids
119
+ batch_delete_item(table => id_with_partitions(ids))
120
+ else
121
+ delete_item(table, ids, options)
122
+ end
123
+ end
124
+ end
125
+
126
+ # Scans a table. Generally quite slow; try to avoid using scan if at all possible.
127
+ #
128
+ # @param [String] table the name of the table to write the object to
129
+ # @param [Hash] scan_hash a hash of attributes: matching records will be returned by the scan
130
+ #
131
+ # @since 0.2.0
132
+ def scan(table, query, opts = {})
133
+ if Dynamoid::Config.partitioning?
134
+ results = benchmark('Scan', table, query) {adapter.scan(table, query, opts)}
135
+ result_for_partition(results,table)
136
+ else
137
+ benchmark('Scan', table, query) {adapter.scan(table, query, opts)}
138
+ end
139
+ end
140
+
141
+ [:batch_get_item, :create_table, :delete_item, :delete_table, :get_item, :list_tables, :put_item].each do |m|
142
+ # Method delegation with benchmark to the underlying adapter. Faster than relying on method_missing.
143
+ #
144
+ # @since 0.2.0
145
+ define_method(m) do |*args|
146
+ benchmark("#{m.to_s}", args) {adapter.send(m, *args)}
147
+ end
148
+ end
149
+
150
+ # Takes a list of ids and returns them with partitioning added. If an array of arrays is passed, we assume the second key is the range key
151
+ # and pass it in unchanged.
152
+ #
153
+ # @example Partition id 1
154
+ # Dynamoid::Adapter.id_with_partitions(['1']) # ['1.0', '1.1', '1.2', ..., '1.199']
155
+ # @example Partition id 1 and range_key 1.0
156
+ # Dynamoid::Adapter.id_with_partitions([['1', 1.0]]) # [['1.0', 1.0], ['1.1', 1.0], ['1.2', 1.0], ..., ['1.199', 1.0]]
157
+ #
158
+ # @param [Array] ids array of ids to partition
159
+ #
160
+ # @since 0.2.0
161
+ def id_with_partitions(ids)
162
+ Array(ids).collect {|id| (0...Dynamoid::Config.partition_size).collect{|n| id.is_a?(Array) ? ["#{id.first}.#{n}", id.last] : "#{id}.#{n}"}}.flatten(1)
163
+ end
164
+
165
+ #Get original id (hash_key) and partiton number from a hash_key
166
+ #
167
+ # @param [String] id the id or hash_key of a record, ex. xxxxx.13
168
+ #
169
+ # @return [String,String] original_id and the partition number, ex original_id = xxxxx partition = 13
170
+ def get_original_id_and_partition id
171
+ partition = id.split('.').last
172
+ id = id.split(".#{partition}").first
173
+
174
+ return id, partition
175
+ end
176
+
177
+ # Takes an array of query results that are partitioned, find the most recently updated ones that share an id and range_key, and return only the most recently updated. Compares each result by
178
+ # their id and updated_at attributes; if the updated_at is the greatest, then it must be the correct result.
179
+ #
180
+ # @param [Array] returned partitioned results from a query
181
+ # @param [String] table_name the name of the table
182
+ #
183
+ # @since 0.2.0
184
+ def result_for_partition(results, table_name)
185
+ table = adapter.get_table(table_name)
186
+
187
+ if table.range_key
188
+ range_key_name = table.range_key.name.to_sym
189
+
190
+ final_hash = {}
191
+
192
+ results.each do |record|
193
+ test_record = final_hash[record[range_key_name]]
194
+
195
+ if test_record.nil? || ((record[range_key_name] == test_record[range_key_name]) && (record[:updated_at] > test_record[:updated_at]))
196
+ #get ride of our partition and put it in the array with the range key
197
+ record[:id], partition = get_original_id_and_partition record[:id]
198
+ final_hash[record[range_key_name]] = record
199
+ end
200
+ end
201
+
202
+ return final_hash.values
203
+ else
204
+ {}.tap do |hash|
205
+ Array(results).each do |result|
206
+ next if result.nil?
207
+ #Need to find the value of id with out the . and partition number
208
+ id, partition = get_original_id_and_partition result[:id]
209
+
210
+ if !hash[id] || (result[:updated_at] > hash[id][:updated_at])
211
+ result[:id] = id
212
+ hash[id] = result
213
+ end
214
+ end
215
+ end.values
216
+ end
217
+ end
218
+
219
+ # Delegate all methods that aren't defind here to the underlying adapter.
220
+ #
221
+ # @since 0.2.0
222
+ def method_missing(method, *args, &block)
223
+ return benchmark(method, *args) {adapter.send(method, *args, &block)} if @adapter.respond_to?(method)
224
+ super
225
+ end
226
+
227
+ # Query the DynamoDB table. This employs DynamoDB's indexes so is generally faster than scanning, but is
228
+ # only really useful for range queries, since it can only find by one hash key at once. Only provide
229
+ # one range key to the hash. If paritioning is on, will run a query for every parition and join the results
230
+ #
231
+ # @param [String] table_name the name of the table
232
+ # @param [Hash] opts the options to query the table with
233
+ # @option opts [String] :hash_value the value of the hash key to find
234
+ # @option opts [Range] :range_value find the range key within this range
235
+ # @option opts [Number] :range_greater_than find range keys greater than this
236
+ # @option opts [Number] :range_less_than find range keys less than this
237
+ # @option opts [Number] :range_gte find range keys greater than or equal to this
238
+ # @option opts [Number] :range_lte find range keys less than or equal to this
239
+ #
240
+ # @return [Array] an array of all matching items
241
+ #
242
+ def query(table_name, opts = {})
243
+
244
+ unless Dynamoid::Config.partitioning?
245
+ #no paritioning? just pass to the standard query method
246
+ adapter.query(table_name, opts)
247
+ else
248
+ #get all the hash_values that could be possible
249
+ ids = id_with_partitions(opts[:hash_value])
250
+
251
+ #lets not overwrite with the original options
252
+ modified_options = opts.clone
253
+ results = []
254
+
255
+ #loop and query on each of the partition ids
256
+ ids.each do |id|
257
+ modified_options[:hash_value] = id
258
+
259
+ query_result = adapter.query(table_name, modified_options)
260
+ results += query_result.inject([]){|array, result| array += [result]} if query_result.any?
261
+ end
262
+
263
+ result_for_partition results, table_name
264
+ end
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,309 @@
1
+ # encoding: utf-8
2
+ require 'aws'
3
+
4
+ module Dynamoid
5
+ module Adapter
6
+
7
+ # The AwsSdk adapter provides support for the AWS-SDK for Ruby gem.
8
+ # More information is available at that Gem's Github page:
9
+ # https://github.com/amazonwebservices/aws-sdk-for-ruby
10
+ #
11
+ module AwsSdk
12
+ extend self
13
+ @@connection = nil
14
+
15
+ # Establish the connection to DynamoDB.
16
+ #
17
+ # @return [AWS::DynamoDB::Connection] the raw DynamoDB connection
18
+ # Call DynamoDB new, with no parameters.
19
+ # Make sure the aws.yml file or aws.rb file, refer the link for more details.
20
+ #https://github.com/amazonwebservices/aws-sdk-for-ruby
21
+ # 1. Create config/aws.yml as follows:
22
+ # Fill in your AWS Access Key ID and Secret Access Key
23
+ # http://aws.amazon.com/security-credentials
24
+ #access_key_id: REPLACE_WITH_ACCESS_KEY_ID
25
+ #secret_access_key: REPLACE_WITH_SECRET_ACCESS_KEY
26
+ #(or)
27
+ #2, Create config/initializers/aws.rb as follows:
28
+ # load the libraries
29
+ #require 'aws'
30
+ # log requests using the default rails logger
31
+ #AWS.config(:logger => Rails.logger)
32
+ # load credentials from a file
33
+ #config_path = File.expand_path(File.dirname(__FILE__)+"/../aws.yml")
34
+ #AWS.config(YAML.load(File.read(config_path)))
35
+ #Additionally include any of the dynamodb paramters as needed
36
+ #(eg: if you would like to change the dynamodb endpoint, then add the parameter in
37
+ # the following paramter in the file aws.yml or aws.rb
38
+ # dynamo_db_endpoint : dynamodb.ap-southeast-1.amazonaws.com)
39
+ # @since 0.2.0
40
+ def connect!
41
+ @@connection = AWS::DynamoDB.new
42
+ end
43
+
44
+ # Return the established connection.
45
+ #
46
+ # @return [AWS::DynamoDB::Connection] the raw DynamoDB connection
47
+ #
48
+ # @since 0.2.0
49
+ def connection
50
+ @@connection
51
+ end
52
+
53
+ # Get many items at once from DynamoDB. More efficient than getting each item individually.
54
+ #
55
+ # @example Retrieve IDs 1 and 2 from the table testtable
56
+ # Dynamoid::Adapter::AwsSdk.batch_get_item({'table1' => ['1', '2']}, :consistent_read => true)
57
+ #
58
+ # @param [Hash] table_ids the hash of tables and IDs to retrieve
59
+ # @param [Hash] options to be passed to underlying BatchGet call
60
+ #
61
+ # @return [Hash] a hash where keys are the table names and the values are the retrieved items
62
+ #
63
+ # @since 0.2.0
64
+ def batch_get_item(table_ids, options = {})
65
+ hash = Hash.new{|h, k| h[k] = []}
66
+ return hash if table_ids.all?{|k, v| v.empty?}
67
+ table_ids.each do |t, ids|
68
+ Array(ids).in_groups_of(100, false) do |group|
69
+ batch = AWS::DynamoDB::BatchGet.new(:config => @@connection.config)
70
+ batch.table(t, :all, Array(group), options) unless group.nil? || group.empty?
71
+ batch.each do |table_name, attributes|
72
+ hash[table_name] << attributes.symbolize_keys!
73
+ end
74
+ end
75
+ end
76
+ hash
77
+ end
78
+
79
+ # Delete many items at once from DynamoDB. More efficient than delete each item individually.
80
+ #
81
+ # @example Delete IDs 1 and 2 from the table testtable
82
+ # Dynamoid::Adapter::AwsSdk.batch_delete_item('table1' => ['1', '2'])
83
+ #or
84
+ # Dynamoid::Adapter::AwsSdk.batch_delete_item('table1' => [['hk1', 'rk2'], ['hk1', 'rk2']]]))
85
+ #
86
+ # @param [Hash] options the hash of tables and IDs to delete
87
+ #
88
+ # @return nil
89
+ #
90
+ def batch_delete_item(options)
91
+ return nil if options.all?{|k, v| v.empty?}
92
+ options.each do |t, ids|
93
+ Array(ids).in_groups_of(25, false) do |group|
94
+ batch = AWS::DynamoDB::BatchWrite.new(:config => @@connection.config)
95
+ batch.delete(t,group)
96
+ batch.process!
97
+ end
98
+ end
99
+ nil
100
+ end
101
+
102
+ # Create a table on DynamoDB. This usually takes a long time to complete.
103
+ #
104
+ # @param [String] table_name the name of the table to create
105
+ # @param [Symbol] key the table's primary key (defaults to :id)
106
+ # @param [Hash] options provide a range_key here if you want one for the table
107
+ #
108
+ # @since 0.2.0
109
+ def create_table(table_name, key = :id, options = {})
110
+ Dynamoid.logger.info "Creating #{table_name} table. This could take a while."
111
+ options[:hash_key] ||= {key.to_sym => :string}
112
+ read_capacity = options[:read_capacity] || Dynamoid::Config.read_capacity
113
+ write_capacity = options[:write_capacity] || Dynamoid::Config.write_capacity
114
+ table = @@connection.tables.create(table_name, read_capacity, write_capacity, options)
115
+ sleep 0.5 while table.status == :creating
116
+ return table
117
+ end
118
+
119
+ # Removes an item from DynamoDB.
120
+ #
121
+ # @param [String] table_name the name of the table
122
+ # @param [String] key the hash key of the item to delete
123
+ # @param [Number] range_key the range key of the item to delete, required if the table has a composite key
124
+ #
125
+ # @since 0.2.0
126
+ def delete_item(table_name, key, options = {})
127
+ range_key = options.delete(:range_key)
128
+ table = get_table(table_name)
129
+ result = table.items.at(key, range_key)
130
+ result.delete unless result.attributes.to_h.empty?
131
+ true
132
+ end
133
+
134
+ # Deletes an entire table from DynamoDB. Only 10 tables can be in the deleting state at once,
135
+ # so if you have more this method may raise an exception.
136
+ #
137
+ # @param [String] table_name the name of the table to destroy
138
+ #
139
+ # @since 0.2.0
140
+ def delete_table(table_name)
141
+ Dynamoid.logger.info "Deleting #{table_name} table. This could take a while."
142
+ table = @@connection.tables[table_name]
143
+ table.delete
144
+ sleep 0.5 while table.exists? == true
145
+ end
146
+
147
+ # @todo Add a DescribeTable method.
148
+
149
+ # Fetches an item from DynamoDB.
150
+ #
151
+ # @param [String] table_name the name of the table
152
+ # @param [String] key the hash key of the item to find
153
+ # @param [Number] range_key the range key of the item to find, required if the table has a composite key
154
+ #
155
+ # @return [Hash] a hash representing the raw item in DynamoDB
156
+ #
157
+ # @since 0.2.0
158
+ def get_item(table_name, key, options = {})
159
+ range_key = options.delete(:range_key)
160
+ table = get_table(table_name)
161
+
162
+ result = table.items.at(key, range_key).attributes.to_h(options)
163
+
164
+ if result.empty?
165
+ nil
166
+ else
167
+ result.symbolize_keys!
168
+ end
169
+ end
170
+
171
+ def update_item(table_name, key, options = {}, &block)
172
+ range_key = options.delete(:range_key)
173
+ conditions = options.delete(:conditions) || {}
174
+ table = get_table(table_name)
175
+ item = table.items.at(key, range_key)
176
+ item.attributes.update(conditions.merge(:return => :all_new), &block)
177
+ rescue AWS::DynamoDB::Errors::ConditionalCheckFailedException
178
+ raise Dynamoid::Errors::ConditionalCheckFailedException
179
+ end
180
+
181
+ # List all tables on DynamoDB.
182
+ #
183
+ # @since 0.2.0
184
+ def list_tables
185
+ @@connection.tables.collect(&:name)
186
+ end
187
+
188
+ # Persists an item on DynamoDB.
189
+ #
190
+ # @param [String] table_name the name of the table
191
+ # @param [Object] object a hash or Dynamoid object to persist
192
+ #
193
+ # @since 0.2.0
194
+ def put_item(table_name, object, options = nil)
195
+ table = get_table(table_name)
196
+ table.items.create(
197
+ object.delete_if{|k, v| v.nil? || (v.respond_to?(:empty?) && v.empty?)},
198
+ options || {}
199
+ )
200
+ rescue AWS::DynamoDB::Errors::ConditionalCheckFailedException => e
201
+ raise Dynamoid::Errors::ConditionalCheckFailedException
202
+ end
203
+
204
+ # Query the DynamoDB table. This employs DynamoDB's indexes so is generally faster than scanning, but is
205
+ # only really useful for range queries, since it can only find by one hash key at once. Only provide
206
+ # one range key to the hash.
207
+ #
208
+ # @param [String] table_name the name of the table
209
+ # @param [Hash] opts the options to query the table with
210
+ # @option opts [String] :hash_value the value of the hash key to find
211
+ # @option opts [Range] :range_value find the range key within this range
212
+ # @option opts [Number] :range_greater_than find range keys greater than this
213
+ # @option opts [Number] :range_less_than find range keys less than this
214
+ # @option opts [Number] :range_gte find range keys greater than or equal to this
215
+ # @option opts [Number] :range_lte find range keys less than or equal to this
216
+ #
217
+ # @return [Enumerator] an iterator of all matching items
218
+ #
219
+ # @since 0.2.0
220
+ def query(table_name, opts = {})
221
+ table = get_table(table_name)
222
+
223
+ Enumerator.new do |yielder|
224
+ consistent_opts = { :consistent_read => opts[:consistent_read] || false }
225
+ if table.composite_key?
226
+ table.items.query(opts).each do |data|
227
+ if opts.has_key? :select
228
+ yielder.yield data.attributes.symbolize_keys!
229
+ else
230
+ yielder.yield data.attributes.to_h(consistent_opts).symbolize_keys!
231
+ end
232
+ end
233
+ else
234
+ yielder.yield get_item(table_name, opts[:hash_value])
235
+ end
236
+ end
237
+ end
238
+
239
+ # Scan the DynamoDB table. This is usually a very slow operation as it naively filters all data on
240
+ # the DynamoDB servers.
241
+ #
242
+ # @param [String] table_name the name of the table
243
+ # @param [Hash] scan_hash a hash of attributes: matching records will be returned by the scan
244
+ #
245
+ # @return [Enumerator] an iterator of all matching items
246
+ #
247
+ # @since 0.2.0
248
+ def scan(table_name, scan_hash, select_opts)
249
+ table = get_table(table_name)
250
+ Enumerator.new do |yielder|
251
+ # MF: Add support for advanced scan filters
252
+ key = scan_hash.keys[0] if scan_hash && scan_hash.keys
253
+ exp = scan_hash[:expression] if scan_hash.has_key? :expression
254
+ case exp
255
+ when "IN"
256
+ table.items.where(key).in(*scan_hash[key]).select(select_opts).each do |data|
257
+ yielder.yield data.attributes.symbolize_keys!
258
+ end
259
+ when "BETWEEN"
260
+ table.items.where(key).between(scan_hash[key][0], scan_hash[key][1]).select(select_opts).each do |data|
261
+ yielder.yield data.attributes.symbolize_keys!
262
+ end
263
+ when "BEGINS_WITH"
264
+ table.items.where(key).between(scan_hash[key][0], scan_hash[key][1]).select(select_opts).each do |data|
265
+ yielder.yield data.attributes.symbolize_keys!
266
+ end
267
+ when "CONTAINS"
268
+ table.items.where(key).contains(scan_hash[key]).select(select_opts).each do |data|
269
+ yielder.yield data.attributes.symbolize_keys!
270
+ end
271
+ else
272
+ table.items.where(scan_hash).select(select_opts).each do |data|
273
+ yielder.yield data.attributes.symbolize_keys!
274
+ end
275
+ end
276
+ end
277
+ end
278
+
279
+ # @todo Add an UpdateItem method.
280
+
281
+ # @todo Add an UpdateTable method.
282
+
283
+ def get_table(table_name)
284
+ unless table = table_cache[table_name]
285
+ table = @@connection.tables[table_name]
286
+ table.load_schema
287
+ table_cache[table_name] = table
288
+ end
289
+ table
290
+ end
291
+
292
+ def table_cache
293
+ @table_cache ||= {}
294
+ end
295
+
296
+ # Number of items from a table
297
+ #
298
+ # @param [String] table_name the name of the table
299
+ #
300
+ # @return [Integer] the number of items from a table
301
+ #
302
+ # @since 0.6.1
303
+ def count(table_name)
304
+ table = get_table(table_name)
305
+ table.items.count
306
+ end
307
+ end
308
+ end
309
+ end