dynamoid 0.7.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +2 -24
- data/README.markdown +89 -73
- data/Rakefile +10 -36
- data/dynamoid.gemspec +56 -191
- data/lib/dynamoid.rb +6 -4
- data/lib/dynamoid/adapter.rb +64 -150
- data/lib/dynamoid/adapter_plugin/aws_sdk_v2.rb +579 -0
- data/lib/dynamoid/components.rb +0 -1
- data/lib/dynamoid/config.rb +2 -5
- data/lib/dynamoid/criteria.rb +1 -1
- data/lib/dynamoid/criteria/chain.rb +27 -140
- data/lib/dynamoid/document.rb +2 -2
- data/lib/dynamoid/errors.rb +30 -9
- data/lib/dynamoid/fields.rb +15 -3
- data/lib/dynamoid/finders.rb +7 -6
- data/lib/dynamoid/identity_map.rb +1 -5
- data/lib/dynamoid/persistence.rb +108 -93
- metadata +56 -229
- data/.document +0 -5
- data/.rspec +0 -1
- data/.travis.yml +0 -7
- data/Gemfile.lock +0 -81
- data/Gemfile_activemodel4 +0 -24
- data/Gemfile_activemodel4.lock +0 -88
- data/VERSION +0 -1
- data/doc/.nojekyll +0 -0
- data/doc/Dynamoid.html +0 -328
- data/doc/Dynamoid/Adapter.html +0 -1872
- data/doc/Dynamoid/Adapter/AwsSdk.html +0 -2101
- data/doc/Dynamoid/Adapter/Local.html +0 -1574
- data/doc/Dynamoid/Associations.html +0 -138
- data/doc/Dynamoid/Associations/Association.html +0 -847
- data/doc/Dynamoid/Associations/BelongsTo.html +0 -161
- data/doc/Dynamoid/Associations/ClassMethods.html +0 -766
- data/doc/Dynamoid/Associations/HasAndBelongsToMany.html +0 -167
- data/doc/Dynamoid/Associations/HasMany.html +0 -167
- data/doc/Dynamoid/Associations/HasOne.html +0 -161
- data/doc/Dynamoid/Associations/ManyAssociation.html +0 -1684
- data/doc/Dynamoid/Associations/SingleAssociation.html +0 -627
- data/doc/Dynamoid/Components.html +0 -242
- data/doc/Dynamoid/Config.html +0 -412
- data/doc/Dynamoid/Config/Options.html +0 -638
- data/doc/Dynamoid/Criteria.html +0 -138
- data/doc/Dynamoid/Criteria/Chain.html +0 -1471
- data/doc/Dynamoid/Criteria/ClassMethods.html +0 -105
- data/doc/Dynamoid/Dirty.html +0 -424
- data/doc/Dynamoid/Dirty/ClassMethods.html +0 -174
- data/doc/Dynamoid/Document.html +0 -1033
- data/doc/Dynamoid/Document/ClassMethods.html +0 -1116
- data/doc/Dynamoid/Errors.html +0 -125
- data/doc/Dynamoid/Errors/ConditionalCheckFailedException.html +0 -141
- data/doc/Dynamoid/Errors/DocumentNotValid.html +0 -221
- data/doc/Dynamoid/Errors/Error.html +0 -137
- data/doc/Dynamoid/Errors/InvalidField.html +0 -141
- data/doc/Dynamoid/Errors/InvalidQuery.html +0 -131
- data/doc/Dynamoid/Errors/MissingRangeKey.html +0 -141
- data/doc/Dynamoid/Fields.html +0 -686
- data/doc/Dynamoid/Fields/ClassMethods.html +0 -438
- data/doc/Dynamoid/Finders.html +0 -135
- data/doc/Dynamoid/Finders/ClassMethods.html +0 -943
- data/doc/Dynamoid/IdentityMap.html +0 -492
- data/doc/Dynamoid/IdentityMap/ClassMethods.html +0 -534
- data/doc/Dynamoid/Indexes.html +0 -321
- data/doc/Dynamoid/Indexes/ClassMethods.html +0 -369
- data/doc/Dynamoid/Indexes/Index.html +0 -1142
- data/doc/Dynamoid/Middleware.html +0 -115
- data/doc/Dynamoid/Middleware/IdentityMap.html +0 -264
- data/doc/Dynamoid/Persistence.html +0 -892
- data/doc/Dynamoid/Persistence/ClassMethods.html +0 -836
- data/doc/Dynamoid/Validations.html +0 -415
- data/doc/_index.html +0 -506
- data/doc/class_list.html +0 -53
- data/doc/css/common.css +0 -1
- data/doc/css/full_list.css +0 -57
- data/doc/css/style.css +0 -338
- data/doc/file.LICENSE.html +0 -73
- data/doc/file.README.html +0 -416
- data/doc/file_list.html +0 -58
- data/doc/frames.html +0 -28
- data/doc/index.html +0 -416
- data/doc/js/app.js +0 -214
- data/doc/js/full_list.js +0 -178
- data/doc/js/jquery.js +0 -4
- data/doc/method_list.html +0 -1144
- data/doc/top-level-namespace.html +0 -112
- data/lib/dynamoid/adapter/aws_sdk.rb +0 -287
- data/lib/dynamoid/indexes.rb +0 -69
- data/lib/dynamoid/indexes/index.rb +0 -103
- data/spec/app/models/address.rb +0 -13
- data/spec/app/models/camel_case.rb +0 -34
- data/spec/app/models/car.rb +0 -6
- data/spec/app/models/magazine.rb +0 -11
- data/spec/app/models/message.rb +0 -9
- data/spec/app/models/nuclear_submarine.rb +0 -5
- data/spec/app/models/sponsor.rb +0 -8
- data/spec/app/models/subscription.rb +0 -12
- data/spec/app/models/tweet.rb +0 -12
- data/spec/app/models/user.rb +0 -26
- data/spec/app/models/vehicle.rb +0 -7
- data/spec/dynamoid/adapter/aws_sdk_spec.rb +0 -376
- data/spec/dynamoid/adapter_spec.rb +0 -155
- data/spec/dynamoid/associations/association_spec.rb +0 -194
- data/spec/dynamoid/associations/belongs_to_spec.rb +0 -71
- data/spec/dynamoid/associations/has_and_belongs_to_many_spec.rb +0 -47
- data/spec/dynamoid/associations/has_many_spec.rb +0 -42
- data/spec/dynamoid/associations/has_one_spec.rb +0 -45
- data/spec/dynamoid/associations_spec.rb +0 -16
- data/spec/dynamoid/config_spec.rb +0 -27
- data/spec/dynamoid/criteria/chain_spec.rb +0 -210
- data/spec/dynamoid/criteria_spec.rb +0 -75
- data/spec/dynamoid/dirty_spec.rb +0 -57
- data/spec/dynamoid/document_spec.rb +0 -180
- data/spec/dynamoid/fields_spec.rb +0 -156
- data/spec/dynamoid/finders_spec.rb +0 -147
- data/spec/dynamoid/identity_map_spec.rb +0 -45
- data/spec/dynamoid/indexes/index_spec.rb +0 -104
- data/spec/dynamoid/indexes_spec.rb +0 -25
- data/spec/dynamoid/persistence_spec.rb +0 -301
- data/spec/dynamoid/validations_spec.rb +0 -36
- data/spec/dynamoid_spec.rb +0 -9
- data/spec/spec_helper.rb +0 -55
- data/spec/support/with_partitioning.rb +0 -15
data/lib/dynamoid.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require "delegate"
|
|
2
2
|
require "time"
|
|
3
3
|
require "securerandom"
|
|
4
|
+
require "active_support"
|
|
4
5
|
require "active_support/core_ext"
|
|
5
6
|
require 'active_support/json'
|
|
6
7
|
require "active_support/inflector"
|
|
@@ -10,7 +11,6 @@ require "active_model"
|
|
|
10
11
|
|
|
11
12
|
require 'dynamoid/errors'
|
|
12
13
|
require 'dynamoid/fields'
|
|
13
|
-
require 'dynamoid/indexes'
|
|
14
14
|
require 'dynamoid/associations'
|
|
15
15
|
require 'dynamoid/persistence'
|
|
16
16
|
require 'dynamoid/dirty'
|
|
@@ -29,10 +29,9 @@ module Dynamoid
|
|
|
29
29
|
extend self
|
|
30
30
|
|
|
31
31
|
MAX_ITEM_SIZE = 65_536
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
def configure
|
|
34
34
|
block_given? ? yield(Dynamoid::Config) : Dynamoid::Config
|
|
35
|
-
Dynamoid::Adapter.reconnect!
|
|
36
35
|
end
|
|
37
36
|
alias :config :configure
|
|
38
37
|
|
|
@@ -43,5 +42,8 @@ module Dynamoid
|
|
|
43
42
|
def included_models
|
|
44
43
|
@included_models ||= []
|
|
45
44
|
end
|
|
46
|
-
|
|
45
|
+
|
|
46
|
+
def adapter
|
|
47
|
+
@adapter ||= Adapter.new
|
|
48
|
+
end
|
|
47
49
|
end
|
data/lib/dynamoid/adapter.rb
CHANGED
|
@@ -1,28 +1,42 @@
|
|
|
1
|
+
# require only 'concurrent/atom' once this issue is resolved:
|
|
2
|
+
# https://github.com/ruby-concurrency/concurrent-ruby/pull/377
|
|
3
|
+
require 'concurrent'
|
|
4
|
+
|
|
1
5
|
# encoding: utf-8
|
|
2
6
|
module Dynamoid
|
|
3
7
|
|
|
4
|
-
# Adapter
|
|
5
|
-
# to
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
# Adapter's value-add:
|
|
9
|
+
# 1) For the rest of Dynamoid, the gateway to DynamoDB.
|
|
10
|
+
# 2) Allows switching `config.adapter` to ease development of a new adapter.
|
|
11
|
+
# 3) Caches the list of tables Dynamoid knows about.
|
|
12
|
+
class Adapter
|
|
13
|
+
def initialize
|
|
14
|
+
@adapter_ = Concurrent::Atom.new(nil)
|
|
15
|
+
@tables_ = Concurrent::Atom.new(nil)
|
|
16
|
+
end
|
|
9
17
|
|
|
10
|
-
|
|
18
|
+
def tables
|
|
19
|
+
if !@tables_.value
|
|
20
|
+
@tables_.swap{|value, args| benchmark('Cache Tables') {list_tables}}
|
|
21
|
+
end
|
|
22
|
+
@tables_.value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The actual adapter currently in use.
|
|
11
26
|
#
|
|
12
27
|
# @since 0.2.0
|
|
13
28
|
def adapter
|
|
14
|
-
|
|
15
|
-
|
|
29
|
+
if !@adapter_.value
|
|
30
|
+
adapter = self.class.adapter_plugin_class.new
|
|
31
|
+
adapter.connect! if adapter.respond_to?(:connect!)
|
|
32
|
+
@adapter_.compare_and_set(nil, adapter)
|
|
33
|
+
clear_cache!
|
|
34
|
+
end
|
|
35
|
+
@adapter_.value
|
|
16
36
|
end
|
|
17
37
|
|
|
18
|
-
|
|
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}
|
|
38
|
+
def clear_cache!
|
|
39
|
+
@tables_.swap{|value, args| nil}
|
|
26
40
|
end
|
|
27
41
|
|
|
28
42
|
# Shows how long it takes a method to run on the adapter. Useful for generating logged output.
|
|
@@ -41,7 +55,7 @@ module Dynamoid
|
|
|
41
55
|
return result
|
|
42
56
|
end
|
|
43
57
|
|
|
44
|
-
# Write an object to the adapter.
|
|
58
|
+
# Write an object to the adapter.
|
|
45
59
|
#
|
|
46
60
|
# @param [String] table the name of the table to write the object to
|
|
47
61
|
# @param [Object] object the object itself
|
|
@@ -51,22 +65,19 @@ module Dynamoid
|
|
|
51
65
|
#
|
|
52
66
|
# @since 0.2.0
|
|
53
67
|
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
68
|
put_item(table, object, options)
|
|
59
69
|
end
|
|
60
70
|
|
|
61
|
-
# Read one or many keys from the selected table.
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
71
|
+
# Read one or many keys from the selected table.
|
|
72
|
+
# This method intelligently calls batch_get or get on the underlying adapter
|
|
73
|
+
# depending on whether ids is a range or a single key.
|
|
74
|
+
# If a range key is present, it will also interpolate that into the ids so
|
|
75
|
+
# that the batch get will acquire the correct record.
|
|
65
76
|
#
|
|
66
77
|
# @param [String] table the name of the table to write the object to
|
|
67
78
|
# @param [Array] ids to fetch, can also be a string of just one id
|
|
68
79
|
# @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
|
|
80
|
+
# unless multiple ids are passed in.
|
|
70
81
|
#
|
|
71
82
|
# @since 0.2.0
|
|
72
83
|
def read(table, ids, options = {})
|
|
@@ -74,25 +85,14 @@ module Dynamoid
|
|
|
74
85
|
|
|
75
86
|
if ids.respond_to?(:each)
|
|
76
87
|
ids = ids.collect{|id| range_key ? [id, range_key] : id}
|
|
77
|
-
|
|
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
|
|
88
|
+
batch_get_item({table => ids}, options)
|
|
83
89
|
else
|
|
84
|
-
if
|
|
85
|
-
|
|
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
|
|
90
|
+
options[:range_key] = range_key if range_key
|
|
91
|
+
get_item(table, ids, options)
|
|
92
92
|
end
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
-
# Delete an item from a table.
|
|
95
|
+
# Delete an item from a table.
|
|
96
96
|
#
|
|
97
97
|
# @param [String] table the name of the table to write the object to
|
|
98
98
|
# @param [Array] ids to delete, can also be a string of just one id
|
|
@@ -107,19 +107,10 @@ module Dynamoid
|
|
|
107
107
|
else
|
|
108
108
|
ids = range_key ? [[ids, range_key]] : ids
|
|
109
109
|
end
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
batch_delete_item(table => id_with_partitions(ids))
|
|
113
|
-
else
|
|
114
|
-
batch_delete_item(table => ids)
|
|
115
|
-
end
|
|
110
|
+
|
|
111
|
+
batch_delete_item(table => ids)
|
|
116
112
|
else
|
|
117
|
-
|
|
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
|
|
113
|
+
delete_item(table, ids, options)
|
|
123
114
|
end
|
|
124
115
|
end
|
|
125
116
|
|
|
@@ -130,15 +121,17 @@ module Dynamoid
|
|
|
130
121
|
#
|
|
131
122
|
# @since 0.2.0
|
|
132
123
|
def scan(table, query, opts = {})
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
124
|
+
benchmark('Scan', table, query) {adapter.scan(table, query, opts)}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def create_table(table_name, key, options = {})
|
|
128
|
+
if !tables.include?(table_name)
|
|
129
|
+
benchmark('Create Table') { adapter.create_table(table_name, key, options) }
|
|
130
|
+
tables << table_name
|
|
138
131
|
end
|
|
139
132
|
end
|
|
140
133
|
|
|
141
|
-
[:batch_get_item, :
|
|
134
|
+
[:batch_get_item, :delete_item, :delete_table, :get_item, :list_tables, :put_item].each do |m|
|
|
142
135
|
# Method delegation with benchmark to the underlying adapter. Faster than relying on method_missing.
|
|
143
136
|
#
|
|
144
137
|
# @since 0.2.0
|
|
@@ -147,86 +140,17 @@ module Dynamoid
|
|
|
147
140
|
end
|
|
148
141
|
end
|
|
149
142
|
|
|
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 = Dynamoid::Adapter::AwsSdk.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
143
|
# Delegate all methods that aren't defind here to the underlying adapter.
|
|
220
144
|
#
|
|
221
145
|
# @since 0.2.0
|
|
222
146
|
def method_missing(method, *args, &block)
|
|
223
|
-
return benchmark(method, *args) {adapter.send(method, *args, &block)} if
|
|
147
|
+
return benchmark(method, *args) {adapter.send(method, *args, &block)} if adapter.respond_to?(method)
|
|
224
148
|
super
|
|
225
149
|
end
|
|
226
|
-
|
|
150
|
+
|
|
227
151
|
# Query the DynamoDB table. This employs DynamoDB's indexes so is generally faster than scanning, but is
|
|
228
152
|
# 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.
|
|
153
|
+
# one range key to the hash.
|
|
230
154
|
#
|
|
231
155
|
# @param [String] table_name the name of the table
|
|
232
156
|
# @param [Hash] opts the options to query the table with
|
|
@@ -240,28 +164,18 @@ module Dynamoid
|
|
|
240
164
|
# @return [Array] an array of all matching items
|
|
241
165
|
#
|
|
242
166
|
def query(table_name, opts = {})
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
#no paritioning? just pass to the standard query method
|
|
246
|
-
Dynamoid::Adapter::AwsSdk.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
|
|
167
|
+
adapter.query(table_name, opts)
|
|
168
|
+
end
|
|
258
169
|
|
|
259
|
-
|
|
260
|
-
results += query_result.inject([]){|array, result| array += [result]} if query_result.any?
|
|
261
|
-
end
|
|
170
|
+
private
|
|
262
171
|
|
|
263
|
-
|
|
172
|
+
def self.adapter_plugin_class
|
|
173
|
+
unless Dynamoid.const_defined?(:AdapterPlugin) && Dynamoid::AdapterPlugin.const_defined?(Dynamoid::Config.adapter.camelcase)
|
|
174
|
+
require "dynamoid/adapter_plugin/#{Dynamoid::Config.adapter}"
|
|
264
175
|
end
|
|
176
|
+
|
|
177
|
+
Dynamoid::AdapterPlugin.const_get(Dynamoid::Config.adapter.camelcase)
|
|
265
178
|
end
|
|
179
|
+
|
|
266
180
|
end
|
|
267
181
|
end
|
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
module Dynamoid
|
|
2
|
+
module AdapterPlugin
|
|
3
|
+
|
|
4
|
+
# The AwsSdkV2 adapter provides support for the aws-sdk version 2 for ruby.
|
|
5
|
+
class AwsSdkV2
|
|
6
|
+
attr_reader :table_cache
|
|
7
|
+
|
|
8
|
+
# Establish the connection to DynamoDB.
|
|
9
|
+
#
|
|
10
|
+
# @return [Aws::DynamoDB::Client] the DynamoDB connection
|
|
11
|
+
def connect!
|
|
12
|
+
@client = if Dynamoid::Config.endpoint?
|
|
13
|
+
Aws::DynamoDB::Client.new(endpoint: Dynamoid::Config.endpoint)
|
|
14
|
+
else
|
|
15
|
+
Aws::DynamoDB::Client.new
|
|
16
|
+
end
|
|
17
|
+
@table_cache = {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Return the client object.
|
|
21
|
+
#
|
|
22
|
+
# @since 1.0.0
|
|
23
|
+
def client
|
|
24
|
+
@client
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Get many items at once from DynamoDB. More efficient than getting each item individually.
|
|
28
|
+
#
|
|
29
|
+
# @example Retrieve IDs 1 and 2 from the table testtable
|
|
30
|
+
# Dynamoid::Adapter::AwsSdkV2.batch_get_item({'table1' => ['1', '2']})
|
|
31
|
+
#
|
|
32
|
+
# @param [Hash] table_ids the hash of tables and IDs to retrieve
|
|
33
|
+
# @param [Hash] options to be passed to underlying BatchGet call
|
|
34
|
+
#
|
|
35
|
+
# @return [Hash] a hash where keys are the table names and the values are the retrieved items
|
|
36
|
+
#
|
|
37
|
+
# @since 1.0.0
|
|
38
|
+
#
|
|
39
|
+
# @todo: Provide support for passing options to underlying batch_get_item http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_get_item-instance_method
|
|
40
|
+
def batch_get_item(table_ids, options = {})
|
|
41
|
+
request_items = Hash.new{|h, k| h[k] = []}
|
|
42
|
+
return request_items if table_ids.all?{|k, v| v.empty?}
|
|
43
|
+
|
|
44
|
+
table_ids.each do |t, ids|
|
|
45
|
+
next if ids.empty?
|
|
46
|
+
tbl = describe_table(t)
|
|
47
|
+
hk = tbl.hash_key.to_s
|
|
48
|
+
rng = tbl.range_key.to_s
|
|
49
|
+
|
|
50
|
+
keys = if rng.present?
|
|
51
|
+
Array(ids).map do |h,r|
|
|
52
|
+
{ hk => h, rng => r }
|
|
53
|
+
end
|
|
54
|
+
else
|
|
55
|
+
Array(ids).map do |id|
|
|
56
|
+
{ hk => id }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
request_items[t] = {
|
|
61
|
+
keys: keys
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
results = client.batch_get_item(
|
|
66
|
+
request_items: request_items
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
ret = Hash.new([].freeze) # Default for tables where no rows are returned
|
|
70
|
+
results.data[:responses].each do |table, rows|
|
|
71
|
+
ret[table] = rows.collect { |r| result_item_to_hash(r) }
|
|
72
|
+
end
|
|
73
|
+
ret
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Delete many items at once from DynamoDB. More efficient than delete each item individually.
|
|
77
|
+
#
|
|
78
|
+
# @example Delete IDs 1 and 2 from the table testtable
|
|
79
|
+
# Dynamoid::Adapter::AwsSdk.batch_delete_item('table1' => ['1', '2'])
|
|
80
|
+
#or
|
|
81
|
+
# Dynamoid::Adapter::AwsSdkV2.batch_delete_item('table1' => [['hk1', 'rk2'], ['hk1', 'rk2']]]))
|
|
82
|
+
#
|
|
83
|
+
# @param [Hash] options the hash of tables and IDs to delete
|
|
84
|
+
#
|
|
85
|
+
# @return nil
|
|
86
|
+
#
|
|
87
|
+
# @todo: Provide support for passing options to underlying delete_item http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#delete_item-instance_method
|
|
88
|
+
def batch_delete_item(options)
|
|
89
|
+
options.each_pair do |table_name, ids|
|
|
90
|
+
table = describe_table(table_name)
|
|
91
|
+
ids.each do |id|
|
|
92
|
+
client.delete_item(table_name: table_name, key: key_stanza(table, *id))
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Create a table on DynamoDB. This usually takes a long time to complete.
|
|
99
|
+
#
|
|
100
|
+
# @param [String] table_name the name of the table to create
|
|
101
|
+
# @param [Symbol] key the table's primary key (defaults to :id)
|
|
102
|
+
# @param [Hash] options provide a range key here if the table has a composite key
|
|
103
|
+
#
|
|
104
|
+
# @since 1.0.0
|
|
105
|
+
def create_table(table_name, key = :id, options = {})
|
|
106
|
+
Dynamoid.logger.info "Creating #{table_name} table. This could take a while."
|
|
107
|
+
read_capacity = options[:read_capacity] || Dynamoid::Config.read_capacity
|
|
108
|
+
write_capacity = options[:write_capacity] || Dynamoid::Config.write_capacity
|
|
109
|
+
range_key = options[:range_key]
|
|
110
|
+
|
|
111
|
+
key_schema = [
|
|
112
|
+
{ attribute_name: key.to_s, key_type: HASH_KEY }
|
|
113
|
+
]
|
|
114
|
+
key_schema << {
|
|
115
|
+
attribute_name: range_key.keys.first.to_s, key_type: RANGE_KEY
|
|
116
|
+
} if(range_key)
|
|
117
|
+
|
|
118
|
+
#TODO: Provide support for number and binary hash key
|
|
119
|
+
attribute_definitions = [
|
|
120
|
+
{ attribute_name: key.to_s, attribute_type: 'S' }
|
|
121
|
+
]
|
|
122
|
+
attribute_definitions << {
|
|
123
|
+
attribute_name: range_key.keys.first.to_s, attribute_type: api_type(range_key.values.first)
|
|
124
|
+
} if(range_key)
|
|
125
|
+
|
|
126
|
+
client.create_table(table_name: table_name,
|
|
127
|
+
provisioned_throughput: {
|
|
128
|
+
read_capacity_units: read_capacity,
|
|
129
|
+
write_capacity_units: write_capacity
|
|
130
|
+
},
|
|
131
|
+
key_schema: key_schema,
|
|
132
|
+
attribute_definitions: attribute_definitions
|
|
133
|
+
)
|
|
134
|
+
rescue Aws::DynamoDB::Errors::ResourceInUseException => e
|
|
135
|
+
Dynamoid.logger.error "Table #{table_name} cannot be created as it already exists"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Removes an item from DynamoDB.
|
|
139
|
+
#
|
|
140
|
+
# @param [String] table_name the name of the table
|
|
141
|
+
# @param [String] key the hash key of the item to delete
|
|
142
|
+
# @param [Hash] options provide a range key here if the table has a composite key
|
|
143
|
+
#
|
|
144
|
+
# @since 1.0.0
|
|
145
|
+
#
|
|
146
|
+
# @todo: Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#delete_item-instance_method
|
|
147
|
+
def delete_item(table_name, key, options = nil)
|
|
148
|
+
table = describe_table(table_name)
|
|
149
|
+
client.delete_item(table_name: table_name, key: key_stanza(table, key, options && options[:range_key]))
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Deletes an entire table from DynamoDB.
|
|
153
|
+
#
|
|
154
|
+
# @param [String] table_name the name of the table to destroy
|
|
155
|
+
#
|
|
156
|
+
# @since 1.0.0
|
|
157
|
+
def delete_table(table_name)
|
|
158
|
+
client.delete_table(table_name: table_name)
|
|
159
|
+
table_cache.clear
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# @todo Add a DescribeTable method.
|
|
163
|
+
|
|
164
|
+
# Fetches an item from DynamoDB.
|
|
165
|
+
#
|
|
166
|
+
# @param [String] table_name the name of the table
|
|
167
|
+
# @param [String] key the hash key of the item to find
|
|
168
|
+
# @param [Hash] options provide a range key here if the table has a composite key
|
|
169
|
+
#
|
|
170
|
+
# @return [Hash] a hash representing the raw item in DynamoDB
|
|
171
|
+
#
|
|
172
|
+
# @since 1.0.0
|
|
173
|
+
#
|
|
174
|
+
# @todo Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#get_item-instance_method
|
|
175
|
+
def get_item(table_name, key, options = {})
|
|
176
|
+
table = describe_table(table_name)
|
|
177
|
+
range_key = options.delete(:range_key)
|
|
178
|
+
|
|
179
|
+
item = client.get_item(table_name: table_name,
|
|
180
|
+
key: key_stanza(table, key, range_key)
|
|
181
|
+
)[:item]
|
|
182
|
+
item ? result_item_to_hash(item) : nil
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Edits an existing item's attributes, or adds a new item to the table if it does not already exist. You can put, delete, or add attribute values
|
|
186
|
+
#
|
|
187
|
+
# @param [String] table_name the name of the table
|
|
188
|
+
# @param [String] key the hash key of the item to find
|
|
189
|
+
# @param [Hash] options provide a range key here if the table has a composite key
|
|
190
|
+
#
|
|
191
|
+
# @return new attributes for the record
|
|
192
|
+
#
|
|
193
|
+
# @todo Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#update_item-instance_method
|
|
194
|
+
def update_item(table_name, key, options = {})
|
|
195
|
+
range_key = options.delete(:range_key)
|
|
196
|
+
conditions = options.delete(:conditions)
|
|
197
|
+
table = describe_table(table_name)
|
|
198
|
+
|
|
199
|
+
yield(iu = ItemUpdater.new(table, key, range_key))
|
|
200
|
+
|
|
201
|
+
raise "non-empty options: #{options}" unless options.empty?
|
|
202
|
+
begin
|
|
203
|
+
result = client.update_item(table_name: table_name,
|
|
204
|
+
key: key_stanza(table, key, range_key),
|
|
205
|
+
attribute_updates: iu.to_h,
|
|
206
|
+
expected: expected_stanza(conditions),
|
|
207
|
+
return_values: "ALL_NEW"
|
|
208
|
+
)
|
|
209
|
+
result_item_to_hash(result[:attributes])
|
|
210
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
|
|
211
|
+
raise Dynamoid::Errors::ConditionalCheckFailedException, e
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# List all tables on DynamoDB.
|
|
216
|
+
#
|
|
217
|
+
# @since 1.0.0
|
|
218
|
+
#
|
|
219
|
+
# @todo Provide limit support http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#update_item-instance_method
|
|
220
|
+
def list_tables
|
|
221
|
+
client.list_tables[:table_names]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Persists an item on DynamoDB.
|
|
225
|
+
#
|
|
226
|
+
# @param [String] table_name the name of the table
|
|
227
|
+
# @param [Object] object a hash or Dynamoid object to persist
|
|
228
|
+
#
|
|
229
|
+
# @since 1.0.0
|
|
230
|
+
#
|
|
231
|
+
# @todo: Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#put_item-instance_method
|
|
232
|
+
def put_item(table_name, object, options = nil)
|
|
233
|
+
item = {}
|
|
234
|
+
|
|
235
|
+
object.each do |k, v|
|
|
236
|
+
next if v.nil? || (v.respond_to?(:empty?) && v.empty?)
|
|
237
|
+
item[k.to_s] = v
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
begin
|
|
241
|
+
client.put_item(table_name: table_name,
|
|
242
|
+
item: item,
|
|
243
|
+
expected: expected_stanza(options)
|
|
244
|
+
)
|
|
245
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
|
|
246
|
+
raise Dynamoid::Errors::ConditionalCheckFailedException, e
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Query the DynamoDB table. This employs DynamoDB's indexes so is generally faster than scanning, but is
|
|
251
|
+
# only really useful for range queries, since it can only find by one hash key at once. Only provide
|
|
252
|
+
# one range key to the hash.
|
|
253
|
+
#
|
|
254
|
+
# @param [String] table_name the name of the table
|
|
255
|
+
# @param [Hash] opts the options to query the table with
|
|
256
|
+
# @option opts [String] :hash_value the value of the hash key to find
|
|
257
|
+
# @option opts [Number, Number] :range_between find the range key within this range
|
|
258
|
+
# @option opts [Number] :range_greater_than find range keys greater than this
|
|
259
|
+
# @option opts [Number] :range_less_than find range keys less than this
|
|
260
|
+
# @option opts [Number] :range_gte find range keys greater than or equal to this
|
|
261
|
+
# @option opts [Number] :range_lte find range keys less than or equal to this
|
|
262
|
+
#
|
|
263
|
+
# @return [Enumerable] matching items
|
|
264
|
+
#
|
|
265
|
+
# @since 1.0.0
|
|
266
|
+
#
|
|
267
|
+
# @todo Provide support for various other options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#query-instance_method
|
|
268
|
+
def query(table_name, opts = {})
|
|
269
|
+
table = describe_table(table_name)
|
|
270
|
+
hk = table.hash_key.to_s
|
|
271
|
+
rng = table.range_key.to_s
|
|
272
|
+
q = opts.slice(:consistent_read, :scan_index_forward, :limit, :select)
|
|
273
|
+
|
|
274
|
+
opts.delete(:consistent_read)
|
|
275
|
+
opts.delete(:scan_index_forward)
|
|
276
|
+
opts.delete(:limit)
|
|
277
|
+
opts.delete(:select)
|
|
278
|
+
|
|
279
|
+
opts.delete(:next_token).tap do |token|
|
|
280
|
+
break unless token
|
|
281
|
+
q[:exclusive_start_key] = {
|
|
282
|
+
hk => token[:hash_key_element],
|
|
283
|
+
rng => token[:range_key_element]
|
|
284
|
+
}
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
key_conditions = {
|
|
288
|
+
hk => {
|
|
289
|
+
# TODO: Provide option for other operators like NE, IN, LE, etc
|
|
290
|
+
comparison_operator: EQ,
|
|
291
|
+
attribute_value_list: [
|
|
292
|
+
opts.delete(:hash_value).freeze
|
|
293
|
+
]
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
opts.each_pair do |k, v|
|
|
298
|
+
# TODO: ATM, only few comparison operators are supported, provide support for all operators
|
|
299
|
+
next unless(op = RANGE_MAP[k])
|
|
300
|
+
key_conditions[rng] = {
|
|
301
|
+
comparison_operator: op,
|
|
302
|
+
attribute_value_list: [
|
|
303
|
+
opts.delete(k).freeze
|
|
304
|
+
].flatten # Flatten as BETWEEN operator specifies array of two elements
|
|
305
|
+
}
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
q[:table_name] = table_name
|
|
309
|
+
q[:key_conditions] = key_conditions
|
|
310
|
+
|
|
311
|
+
Enumerator.new { |y|
|
|
312
|
+
result = client.query(q)
|
|
313
|
+
|
|
314
|
+
result.items.each { |r|
|
|
315
|
+
y << result_item_to_hash(r)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
EQ = "EQ".freeze
|
|
321
|
+
|
|
322
|
+
RANGE_MAP = {
|
|
323
|
+
range_greater_than: 'GT',
|
|
324
|
+
range_less_than: 'LT',
|
|
325
|
+
range_gte: 'GE',
|
|
326
|
+
range_lte: 'LE',
|
|
327
|
+
range_begins_with: 'BEGINS_WITH',
|
|
328
|
+
range_between: 'BETWEEN'
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
# Scan the DynamoDB table. This is usually a very slow operation as it naively filters all data on
|
|
332
|
+
# the DynamoDB servers.
|
|
333
|
+
#
|
|
334
|
+
# @param [String] table_name the name of the table
|
|
335
|
+
# @param [Hash] scan_hash a hash of attributes: matching records will be returned by the scan
|
|
336
|
+
#
|
|
337
|
+
# @return [Enumerable] matching items
|
|
338
|
+
#
|
|
339
|
+
# @since 1.0.0
|
|
340
|
+
#
|
|
341
|
+
# @todo: Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#scan-instance_method
|
|
342
|
+
def scan(table_name, scan_hash, select_opts = {})
|
|
343
|
+
limit = select_opts.delete(:limit)
|
|
344
|
+
batch = select_opts.delete(:batch_size)
|
|
345
|
+
|
|
346
|
+
request = { table_name: table_name }
|
|
347
|
+
request[:limit] = batch || limit if batch || limit
|
|
348
|
+
request[:scan_filter] = scan_hash.reduce({}) do |memo, kvp|
|
|
349
|
+
memo[kvp[0].to_s] = {
|
|
350
|
+
attribute_value_list: [kvp[1]],
|
|
351
|
+
# TODO: Provide support for all comparison operators
|
|
352
|
+
comparison_operator: EQ
|
|
353
|
+
}
|
|
354
|
+
memo
|
|
355
|
+
end if scan_hash.present?
|
|
356
|
+
|
|
357
|
+
Enumerator.new do |y|
|
|
358
|
+
# Batch loop, pulls multiple requests until done using the start_key
|
|
359
|
+
loop do
|
|
360
|
+
results = client.scan(request)
|
|
361
|
+
|
|
362
|
+
results.data[:items].each { |row| y << result_item_to_hash(row) }
|
|
363
|
+
|
|
364
|
+
if((lk = results[:last_evaluated_key]) && batch)
|
|
365
|
+
request[:exclusive_start_key] = lk
|
|
366
|
+
else
|
|
367
|
+
break
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
#
|
|
375
|
+
# Truncates all records in the given table
|
|
376
|
+
#
|
|
377
|
+
# @param [String] table_name the name of the table
|
|
378
|
+
#
|
|
379
|
+
# @since 1.0.0
|
|
380
|
+
def truncate(table_name)
|
|
381
|
+
table = describe_table(table_name)
|
|
382
|
+
hk = table.hash_key
|
|
383
|
+
rk = table.range_key
|
|
384
|
+
|
|
385
|
+
scan(table_name, {}, {}).each do |attributes|
|
|
386
|
+
opts = {range_key: attributes[rk.to_sym] } if rk
|
|
387
|
+
delete_item(table_name, attributes[hk], opts)
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def count(table_name)
|
|
392
|
+
describe_table(table_name, true).item_count
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
protected
|
|
396
|
+
|
|
397
|
+
STRING_TYPE = "S".freeze
|
|
398
|
+
NUM_TYPE = "N".freeze
|
|
399
|
+
BOOLEAN_TYPE = "B".freeze
|
|
400
|
+
|
|
401
|
+
#Converts from symbol to the API string for the given data type
|
|
402
|
+
# E.g. :number -> 'N'
|
|
403
|
+
def api_type(type)
|
|
404
|
+
case(type)
|
|
405
|
+
when :string then STRING_TYPE
|
|
406
|
+
when :number then NUM_TYPE
|
|
407
|
+
when :datetime then NUM_TYPE
|
|
408
|
+
when :boolean then BOOLEAN_TYPE
|
|
409
|
+
else raise "Unknown type: #{type}"
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
#
|
|
414
|
+
# The key hash passed on get_item, put_item, delete_item, update_item, etc
|
|
415
|
+
#
|
|
416
|
+
def key_stanza(table, hash_key, range_key = nil)
|
|
417
|
+
key = { table.hash_key.to_s => hash_key }
|
|
418
|
+
key[table.range_key.to_s] = range_key if range_key
|
|
419
|
+
key
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
#
|
|
423
|
+
# @param [Hash] conditions Conditions to enforce on operation (e.g. { :if => { :count => 5 }, :unless_exists => ['id']})
|
|
424
|
+
# @return an Expected stanza for the given conditions hash
|
|
425
|
+
#
|
|
426
|
+
def expected_stanza(conditions = nil)
|
|
427
|
+
expected = Hash.new { |h,k| h[k] = {} }
|
|
428
|
+
return expected unless conditions
|
|
429
|
+
|
|
430
|
+
conditions[:unless_exists].try(:each) do |col|
|
|
431
|
+
expected[col.to_s][:exists] = false
|
|
432
|
+
end
|
|
433
|
+
conditions[:if].try(:each) do |col,val|
|
|
434
|
+
expected[col.to_s][:value] = val
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
expected
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
HASH_KEY = "HASH".freeze
|
|
441
|
+
RANGE_KEY = "RANGE".freeze
|
|
442
|
+
|
|
443
|
+
#
|
|
444
|
+
# New, semi-arbitrary API to get data on the table
|
|
445
|
+
#
|
|
446
|
+
def describe_table(table_name, reload = false)
|
|
447
|
+
(!reload && table_cache[table_name]) || begin
|
|
448
|
+
table_cache[table_name] = Table.new(client.describe_table(table_name: table_name).data)
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
#
|
|
453
|
+
# Converts a hash returned by get_item, scan, etc. into a key-value hash
|
|
454
|
+
#
|
|
455
|
+
def result_item_to_hash(item)
|
|
456
|
+
{}.tap do |r|
|
|
457
|
+
item.each { |k,v| r[k.to_sym] = v }
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
#
|
|
462
|
+
# Represents a table. Exposes data from the "DescribeTable" API call, and also
|
|
463
|
+
# provides methods for coercing values to the proper types based on the table's schema data
|
|
464
|
+
#
|
|
465
|
+
class Table
|
|
466
|
+
attr_reader :schema
|
|
467
|
+
|
|
468
|
+
#
|
|
469
|
+
# @param [Hash] schema Data returns from a "DescribeTable" call
|
|
470
|
+
#
|
|
471
|
+
def initialize(schema)
|
|
472
|
+
@schema = schema[:table]
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def range_key
|
|
476
|
+
@range_key ||= schema[:key_schema].find { |d| d[:key_type] == RANGE_KEY }.try(:attribute_name)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def range_type
|
|
480
|
+
range_type ||= schema[:attribute_definitions].find { |d|
|
|
481
|
+
d[:attribute_name] == range_key
|
|
482
|
+
}.try(:fetch,:attribute_type, nil)
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def hash_key
|
|
486
|
+
@hash_key ||= schema[:key_schema].find { |d| d[:key_type] == HASH_KEY }.try(:attribute_name).to_sym
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
#
|
|
490
|
+
# Returns the API type (e.g. "N", "S") for the given column, if the schema defines it,
|
|
491
|
+
# nil otherwise
|
|
492
|
+
#
|
|
493
|
+
def col_type(col)
|
|
494
|
+
col = col.to_s
|
|
495
|
+
col_def = schema[:attribute_definitions].find { |d| d[:attribute_name] == col.to_s }
|
|
496
|
+
col_def && col_def[:attribute_type]
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def item_count
|
|
500
|
+
schema[:item_count]
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
#
|
|
505
|
+
# Mimics behavior of the yielded object on DynamoDB's update_item API (high level).
|
|
506
|
+
#
|
|
507
|
+
class ItemUpdater
|
|
508
|
+
attr_reader :table, :key, :range_key
|
|
509
|
+
|
|
510
|
+
def initialize(table, key, range_key = nil)
|
|
511
|
+
@table = table; @key = key, @range_key = range_key
|
|
512
|
+
@additions = {}
|
|
513
|
+
@deletions = {}
|
|
514
|
+
@updates = {}
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
#
|
|
518
|
+
# Adds the given values to the values already stored in the corresponding columns.
|
|
519
|
+
# The column must contain a Set or a number.
|
|
520
|
+
#
|
|
521
|
+
# @param [Hash] vals keys of the hash are the columns to update, vals are the values to
|
|
522
|
+
# add. values must be a Set, Array, or Numeric
|
|
523
|
+
#
|
|
524
|
+
def add(values)
|
|
525
|
+
@additions.merge!(values)
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
#
|
|
529
|
+
# Removes values from the sets of the given columns
|
|
530
|
+
#
|
|
531
|
+
# @param [Hash] values keys of the hash are the columns, values are Arrays/Sets of items
|
|
532
|
+
# to remove
|
|
533
|
+
#
|
|
534
|
+
def delete(values)
|
|
535
|
+
@deletions.merge!(values)
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
#
|
|
539
|
+
# Replaces the values of one or more attributes
|
|
540
|
+
#
|
|
541
|
+
def set(values)
|
|
542
|
+
@updates.merge!(values)
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
#
|
|
546
|
+
# Returns an AttributeUpdates hash suitable for passing to the V2 Client API
|
|
547
|
+
#
|
|
548
|
+
def to_h
|
|
549
|
+
ret = {}
|
|
550
|
+
|
|
551
|
+
@additions.each do |k,v|
|
|
552
|
+
ret[k.to_s] = {
|
|
553
|
+
action: ADD,
|
|
554
|
+
value: v
|
|
555
|
+
}
|
|
556
|
+
end
|
|
557
|
+
@deletions.each do |k,v|
|
|
558
|
+
ret[k.to_s] = {
|
|
559
|
+
action: DELETE,
|
|
560
|
+
value: v
|
|
561
|
+
}
|
|
562
|
+
end
|
|
563
|
+
@updates.each do |k,v|
|
|
564
|
+
ret[k.to_s] = {
|
|
565
|
+
action: PUT,
|
|
566
|
+
value: v
|
|
567
|
+
}
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
ret
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
ADD = "ADD".freeze
|
|
574
|
+
DELETE = "DELETE".freeze
|
|
575
|
+
PUT = "PUT".freeze
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
end
|