ddbcli 0.1.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.
@@ -0,0 +1,45 @@
1
+ require 'optparse'
2
+ require 'ostruct'
3
+
4
+ def parse_options
5
+ options = OpenStruct.new
6
+ options.access_key_id = ENV['AWS_ACCESS_KEY_ID']
7
+ options.secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
8
+ options.ddb_endpoint_or_region =
9
+ ENV['DDB_ENDPOINT'] || ENV['DDB_REGION'] || 'dynamodb.us-east-1.amazonaws.com'
10
+
11
+ # default value
12
+ options.timeout = 60
13
+ options.consistent = false
14
+ options.retry_num = 3
15
+ options.retry_intvl = 10
16
+ options.debug = false
17
+
18
+ ARGV.options do |opt|
19
+ opt.on('-k', '--access-key=ACCESS_KEY') {|v| options.access_key_id = v }
20
+ opt.on('-s', '--secret-key=SECRET_KEY') {|v| options.secret_access_key = v }
21
+ opt.on('-r', '--region=REGION_OR_ENDPOINT') {|v| options.ddb_endpoint_or_region = v }
22
+ opt.on('-e', '--eval=COMMAND') {|v| options.command = v }
23
+ opt.on('-t', '--timeout=SECOND', Integer) {|v| options.timeout = v.to_i }
24
+ opt.on('', '--consistent-read') { options.consistent = true }
25
+ opt.on('', '--retry=NUM', Integer) {|v| options.retry_num = v.to_i }
26
+ opt.on('', '--retry-interval=SECOND', Integer) {|v| options.retry_intvl = v.to_i }
27
+ opt.on('', '--debug') { options.debug = true }
28
+
29
+ opt.on('-h', '--help') {
30
+ puts opt.help
31
+ puts
32
+ print_help
33
+ exit
34
+ }
35
+
36
+ opt.parse!
37
+
38
+ unless options.access_key_id and options.secret_access_key and options.ddb_endpoint_or_region
39
+ puts opt.help
40
+ exit 1
41
+ end
42
+ end
43
+
44
+ options
45
+ end
@@ -0,0 +1,15 @@
1
+ module DynamoDB
2
+ class Binary
3
+ attr_reader :value
4
+ alias to_s value
5
+ alias to_str value
6
+
7
+ def initialize(value)
8
+ @value = value
9
+ end
10
+
11
+ def inspect
12
+ @value.inspect
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,239 @@
1
+ require 'json'
2
+ require 'openssl'
3
+ require 'net/http'
4
+ require 'time'
5
+ require 'stringio'
6
+ require 'zlib'
7
+ require 'pp'
8
+
9
+ require 'ddbcli/ddb-error'
10
+ require 'ddbcli/ddb-endpoint'
11
+
12
+ module DynamoDB
13
+ class Client
14
+
15
+ SERVICE_NAME = 'dynamodb'
16
+ API_VERSION = '2012-08-10'
17
+ USER_AGENT = "ddbcli/#{Version}"
18
+
19
+ DEFAULT_TIMEOUT = 60
20
+
21
+ attr_reader :endpoint
22
+ attr_reader :region
23
+ attr_accessor :timeout
24
+ attr_accessor :retry_num
25
+ attr_accessor :retry_intvl
26
+ attr_accessor :debug
27
+
28
+ def initialize(accessKeyId, secretAccessKey, endpoint_or_region)
29
+ @accessKeyId = accessKeyId
30
+ @secretAccessKey = secretAccessKey
31
+ set_endpoint_and_region(endpoint_or_region)
32
+ @timeout = DEFAULT_TIMEOUT
33
+ @debug = false
34
+ @retry_num = 3
35
+ @retry_intvl = 10
36
+ end
37
+
38
+ def set_endpoint_and_region(endpoint_or_region)
39
+ @endpoint, @region = DynamoDB::Endpoint.endpoint_and_region(endpoint_or_region)
40
+ end
41
+
42
+ def query(action, hash)
43
+ retry_query do
44
+ query0(action, hash)
45
+ end
46
+ end
47
+
48
+ def query0(action, hash)
49
+ if @debug
50
+ $stderr.puts(<<EOS)
51
+ ---request begin---
52
+ Action: #{action}
53
+ #{hash.pretty_inspect}
54
+ ---request end---
55
+ EOS
56
+ end
57
+
58
+ req_body = JSON.dump(hash)
59
+ date = Time.now.getutc
60
+
61
+ headers = {
62
+ 'Content-Type' => 'application/x-amz-json-1.0',
63
+ 'X-Amz-Target' => "DynamoDB_#{API_VERSION.gsub('-', '')}.#{action}",
64
+ 'Content-Length' => req_body.length.to_s,
65
+ 'User-Agent' => USER_AGENT,
66
+ 'Host' => 'dynamodb.us-east-1.amazonaws.com',
67
+ 'X-Amz-Date' => iso8601(date),
68
+ 'X-Amz-Content-Sha256' => hexhash(req_body),
69
+ 'Accept' => '*/*',
70
+ 'Accept-Encoding' => 'gzip',
71
+ }
72
+
73
+ headers['Authorization'] = authorization(date, headers, req_body)
74
+
75
+ Net::HTTP.version_1_2
76
+ https = Net::HTTP.new(@endpoint, 443)
77
+ https.use_ssl = true
78
+ https.verify_mode = OpenSSL::SSL::VERIFY_NONE
79
+ https.open_timeout = @timeout
80
+ https.read_timeout = @timeout
81
+
82
+ res_code = nil
83
+ res_msg = nil
84
+
85
+ res_body = https.start do |w|
86
+ req = Net::HTTP::Post.new('/', headers)
87
+ req.body = req_body
88
+ res = w.request(req)
89
+
90
+ res_code = res.code.to_i
91
+ res_msg = res.message
92
+
93
+ if res['Content-Encoding'] == 'gzip'
94
+ StringIO.open(res.body, 'rb') do |f|
95
+ Zlib::GzipReader.wrap(f).read
96
+ end
97
+ else
98
+ res.body
99
+ end
100
+ end
101
+
102
+ res_data = JSON.parse(res_body)
103
+
104
+ if @debug
105
+ $stderr.puts(<<EOS)
106
+ ---response begin---
107
+ #{res_data.pretty_inspect}
108
+ ---response end---
109
+ EOS
110
+ end
111
+
112
+ __type = res_data['__type']
113
+
114
+ if res_code != 200 or __type
115
+ errmsg = if __type
116
+ if @debug
117
+ "#{__type}: #{res_data['message'] || res_data['Message']}"
118
+ else
119
+ "#{res_data['message'] || res_data['Message']}"
120
+ end
121
+ else
122
+ "#{res_code} #{res_msg}"
123
+ end
124
+
125
+ raise DynamoDB::Error.new(errmsg, res_data)
126
+ end
127
+
128
+ res_data
129
+ end
130
+
131
+ private
132
+
133
+ def authorization(date, headers, body)
134
+ headers = headers.sort_by {|name, value| name }
135
+
136
+ # Task 1: Create a Canonical Request For Signature Version 4
137
+ # http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
138
+
139
+ canonicalHeaders = headers.map {|name, value|
140
+ name.downcase + ':' + value
141
+ }.join("\n") + "\n"
142
+
143
+ signedHeaders = headers.map {|name, value| name.downcase }.join(';')
144
+
145
+ canonicalRequest = [
146
+ 'POST', # HTTPRequestMethod
147
+ '/', # CanonicalURI
148
+ '', # CanonicalQueryString
149
+ canonicalHeaders,
150
+ signedHeaders,
151
+ hexhash(body),
152
+ ].join("\n")
153
+
154
+ # Task 2: Create a String to Sign for Signature Version 4
155
+ # http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
156
+
157
+ credentialScope = [
158
+ date.strftime('%Y%m%d'),
159
+ @region,
160
+ SERVICE_NAME,
161
+ 'aws4_request',
162
+ ].join('/')
163
+
164
+ stringToSign = [
165
+ 'AWS4-HMAC-SHA256', # Algorithm
166
+ iso8601(date), # RequestDate
167
+ credentialScope,
168
+ hexhash(canonicalRequest),
169
+ ].join("\n")
170
+
171
+ # Task 3: Calculate the AWS Signature Version 4
172
+ # http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
173
+
174
+ kDate = hmac('AWS4' + @secretAccessKey, date.strftime('%Y%m%d'))
175
+ kRegion = hmac(kDate, @region)
176
+ kService = hmac(kRegion, SERVICE_NAME)
177
+ kSigning = hmac(kService, 'aws4_request')
178
+ signature = hexhmac(kSigning, stringToSign)
179
+
180
+ 'AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s' % [
181
+ @accessKeyId,
182
+ credentialScope,
183
+ signedHeaders,
184
+ signature,
185
+ ]
186
+ end
187
+
188
+ def iso8601(utc)
189
+ utc.strftime('%Y%m%dT%H%M%SZ')
190
+ end
191
+
192
+ def hexhash(data)
193
+ OpenSSL::Digest::SHA256.new.hexdigest(data)
194
+ end
195
+
196
+ def hmac(key, data)
197
+ OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, key, data)
198
+ end
199
+
200
+ def hexhmac(key, data)
201
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, key, data)
202
+ end
203
+
204
+ #def escape(str)
205
+ # CGI.escape(str.to_s).gsub('+', '%20')
206
+ #end
207
+
208
+ def retry_query
209
+ retval = nil
210
+
211
+ (@retry_num + 1).times do |i|
212
+ begin
213
+ retval = yield
214
+ break
215
+ rescue Errno::ETIMEDOUT => e
216
+ raise e if i >= @retry_num
217
+ rescue DynamoDB::Error => e
218
+ if [/\bServiceUnavailable\b/i, /\bexceeded\b/i].any? {|i| i =~ e.message }
219
+ raise e if i >= @retry_num
220
+ else
221
+ raise e
222
+ end
223
+ rescue Timeout::Error => e
224
+ raise e if i >= @retry_num
225
+ end
226
+
227
+ wait_sec = @retry_intvl * (i + 1)
228
+
229
+ if @debug
230
+ $stderr.puts("Retry... (wait %d seconds)" % wait_sec)
231
+ end
232
+
233
+ sleep wait_sec
234
+ end
235
+
236
+ return retval
237
+ end
238
+ end # Client
239
+ end # SimpleDB
@@ -0,0 +1,646 @@
1
+ require 'ddbcli/ddb-client'
2
+ require 'ddbcli/ddb-parser.tab'
3
+ require 'ddbcli/ddb-iteratorable'
4
+
5
+ require 'forwardable'
6
+
7
+ module DynamoDB
8
+ class Driver
9
+ extend Forwardable
10
+
11
+ MAX_NUMBER_BATCH_PROCESS_ITEMS = 25
12
+
13
+ class Rownum
14
+ def initialize(rownum)
15
+ @rownum = rownum
16
+ end
17
+
18
+ def to_i
19
+ @rownum
20
+ end
21
+ end # Rownum
22
+
23
+ def initialize(accessKeyId, secretAccessKey, endpoint_or_region)
24
+ @client = DynamoDB::Client.new(accessKeyId, secretAccessKey, endpoint_or_region)
25
+ @consistent = false
26
+ end
27
+
28
+ def_delegators(
29
+ :@client,
30
+ :endpoint,
31
+ :region,
32
+ :timeout, :'timeout=',
33
+ :set_endpoint_and_region,
34
+ :retry_num, :'retry_num=',
35
+ :retry_intvl, :'retry_intvl=',
36
+ :debug, :'debug=')
37
+
38
+ attr_accessor :consistent
39
+
40
+ def execute(query, opts = {})
41
+ parsed, script_type, script = Parser.parse(query)
42
+ command = parsed.class.name.split('::').last.to_sym
43
+
44
+ if command != :NEXT
45
+ @last_action = nil
46
+ @last_parsed = nil
47
+ @last_evaluated_key = nil
48
+ end
49
+
50
+ retval = case command
51
+ when :SHOW_TABLES
52
+ do_show_tables(parsed)
53
+ when :SHOW_REGIONS
54
+ do_show_regions(parsed)
55
+ when :SHOW_CREATE_TABLE
56
+ do_show_create_table(parsed)
57
+ when :ALTER_TABLE
58
+ do_alter_table(parsed)
59
+ when :USE
60
+ do_use(parsed)
61
+ when :CREATE
62
+ do_create(parsed)
63
+ when :DROP
64
+ do_drop(parsed)
65
+ when :DESCRIBE
66
+ do_describe(parsed)
67
+ when :SELECT
68
+ do_select('Query', parsed)
69
+ when :SCAN
70
+ do_select('Scan', parsed)
71
+ when :GET
72
+ do_get(parsed)
73
+ when :UPDATE
74
+ do_update(parsed)
75
+ when :UPDATE_ALL
76
+ do_update_all(parsed)
77
+ when :DELETE
78
+ do_delete(parsed)
79
+ when :DELETE_ALL
80
+ do_delete_all(parsed)
81
+ when :INSERT
82
+ do_insert(parsed)
83
+ when :NEXT
84
+ if @last_action and @last_parsed and @last_evaluated_key
85
+ do_select(@last_action, @last_parsed, :last_evaluated_key => @last_evaluated_key)
86
+ else
87
+ []
88
+ end
89
+ else
90
+ raise 'must not happen'
91
+ end
92
+
93
+ begin
94
+ case script_type
95
+ when :ruby
96
+ retval = retval.data if retval.kind_of?(DynamoDB::Iteratorable)
97
+ retval.instance_eval(script)
98
+ when :shell
99
+ retval = retval.data if retval.kind_of?(DynamoDB::Iteratorable)
100
+ IO.popen(script, "r+") do |f|
101
+ f.puts(retval.kind_of?(Array) ? retval.map {|i| i.to_s }.join("\n") : retval.to_s)
102
+ f.close_write
103
+ f.read
104
+ end
105
+ else
106
+ retval
107
+ end
108
+ rescue Exception => e
109
+ raise DynamoDB::Error, e.message, e.backtrace
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def do_show_tables(parsed)
116
+ req_hash = {}
117
+ table_names = []
118
+
119
+ req_hash['Limit'] = parsed.limit if parsed.limit
120
+
121
+ list = lambda do |last_evaluated_table_name|
122
+ req_hash['ExclusiveStartTableName'] = last_evaluated_table_name if last_evaluated_table_name
123
+ res_data = @client.query('ListTables', req_hash)
124
+ table_names.concat(res_data['TableNames'])
125
+ req_hash['LastEvaluatedTableName']
126
+ end
127
+
128
+ letn = nil
129
+
130
+ loop do
131
+ letn = list.call(letn)
132
+
133
+ if parsed.limit or not letn
134
+ break
135
+ end
136
+ end
137
+
138
+ return table_names
139
+ end
140
+
141
+ def do_show_regions(parsed)
142
+ DynamoDB::Endpoint.regions
143
+ end
144
+
145
+ def do_show_create_table(parsed)
146
+ table_info = @client.query('DescribeTable', 'TableName' => parsed.table)['Table']
147
+ table_name = table_info['TableName']
148
+
149
+ attr_types = {}
150
+ table_info['AttributeDefinitions'].each do |i|
151
+ name = i['AttributeName']
152
+ attr_types[name] = {
153
+ 'S' => 'STRING',
154
+ 'N' => 'NUMBER',
155
+ 'B' => 'BINARY',
156
+ }.fetch(i['AttributeType'])
157
+ end
158
+
159
+ key_schema = {}
160
+ table_info['KeySchema'].map do |i|
161
+ name = i['AttributeName']
162
+ key_type = i['KeyType']
163
+ key_schema[name] = key_type
164
+ end
165
+
166
+ indexes = {}
167
+
168
+ (table_info['LocalSecondaryIndexes'] || []).each do |i|
169
+ index_name = i['IndexName']
170
+ key_name = i['KeySchema'].find {|j| j['KeyType'] == 'RANGE' }['AttributeName']
171
+ proj_type = i['Projection']['ProjectionType']
172
+ proj_attrs = i['Projection']['NonKeyAttributes']
173
+ indexes[index_name] = [key_name, proj_type, proj_attrs]
174
+ end
175
+
176
+ throughput = table_info['ProvisionedThroughput']
177
+ throughput = {
178
+ :read => throughput['ReadCapacityUnits'],
179
+ :write => throughput['WriteCapacityUnits'],
180
+ }
181
+
182
+ quote = lambda {|i| '`' + i.gsub('`', '``') + '`' } # `
183
+
184
+ buf = "CREATE TABLE #{quote[table_name]} ("
185
+
186
+ buf << "\n " + key_schema.map {|name, key_type|
187
+ attr_type = attr_types[name]
188
+ "#{quote[name]} #{attr_type} #{key_type}"
189
+ }.join(",\n ")
190
+
191
+ unless indexes.empty?
192
+ buf << ",\n " + indexes.map {|index_name, key_name_proj|
193
+ key_name, proj_type, proj_attrs = key_name_proj
194
+ attr_type = attr_types[key_name]
195
+ index_clause = "INDEX #{quote[index_name]} (#{quote[key_name]} #{attr_type}) #{proj_type}"
196
+ index_clause << " (#{proj_attrs.join(', ')})" if proj_attrs
197
+ index_clause
198
+ }.join(",\n ")
199
+ end
200
+
201
+ buf << "\n)"
202
+ buf << ' ' + throughput.map {|k, v| "#{k}=#{v}" }.join(', ')
203
+ buf << "\n\n"
204
+
205
+ return buf
206
+ end
207
+
208
+ def do_alter_table(parsed)
209
+ req_hash = {
210
+ 'TableName' => parsed.table,
211
+ 'ProvisionedThroughput' => {
212
+ 'ReadCapacityUnits' => parsed.capacity[:read],
213
+ 'WriteCapacityUnits' => parsed.capacity[:write],
214
+ },
215
+ }
216
+
217
+ @client.query('UpdateTable', req_hash)
218
+ nil
219
+ end
220
+
221
+ def do_use(parsed)
222
+ set_endpoint_and_region(parsed.endpoint_or_region)
223
+ nil
224
+ end
225
+
226
+ def do_create(parsed)
227
+ req_hash = {
228
+ 'TableName' => parsed.table,
229
+ 'ProvisionedThroughput' => {
230
+ 'ReadCapacityUnits' => parsed.capacity[:read],
231
+ 'WriteCapacityUnits' => parsed.capacity[:write],
232
+ },
233
+ }
234
+
235
+ # hash key
236
+ req_hash['AttributeDefinitions'] = [
237
+ {
238
+ 'AttributeName' => parsed.hash[:name],
239
+ 'AttributeType' => parsed.hash[:type],
240
+ }
241
+ ]
242
+
243
+ req_hash['KeySchema'] = [
244
+ {
245
+ 'AttributeName' => parsed.hash[:name],
246
+ 'KeyType' => 'HASH',
247
+ }
248
+ ]
249
+
250
+ # range key
251
+ if parsed.range
252
+ req_hash['AttributeDefinitions'] << {
253
+ 'AttributeName' => parsed.range[:name],
254
+ 'AttributeType' => parsed.range[:type],
255
+ }
256
+
257
+ req_hash['KeySchema'] << {
258
+ 'AttributeName' => parsed.range[:name],
259
+ 'KeyType' => 'RANGE',
260
+ }
261
+ end
262
+
263
+ # local secondary index
264
+ if parsed.indices
265
+ req_hash['LocalSecondaryIndexes'] = []
266
+
267
+ parsed.indices.each do |idx_def|
268
+ req_hash['AttributeDefinitions'] << {
269
+ 'AttributeName' => idx_def[:key],
270
+ 'AttributeType' => idx_def[:type],
271
+ }
272
+
273
+ local_secondary_index = {
274
+ 'IndexName' => idx_def[:name],
275
+ 'KeySchema' => [
276
+ {
277
+ 'AttributeName' => parsed.hash[:name],
278
+ 'KeyType' => 'HASH',
279
+ },
280
+ {
281
+ 'AttributeName' => idx_def[:key],
282
+ 'KeyType' => 'RANGE',
283
+ },
284
+ ],
285
+ 'Projection' => {
286
+ 'ProjectionType' => idx_def[:projection][:type],
287
+ }
288
+ }
289
+
290
+ if idx_def[:projection][:attrs]
291
+ local_secondary_index['Projection']['NonKeyAttributes'] = idx_def[:projection][:attrs]
292
+ end
293
+
294
+ req_hash['LocalSecondaryIndexes'] << local_secondary_index
295
+ end
296
+ end # local secondary index
297
+
298
+ @client.query('CreateTable', req_hash)
299
+ nil
300
+ end
301
+
302
+ def do_drop(parsed)
303
+ @client.query('DeleteTable', 'TableName' => parsed.table)
304
+ nil
305
+ end
306
+
307
+ def do_describe(parsed)
308
+ (@client.query('DescribeTable', 'TableName' => parsed.table) || {}).fetch('Table', {})
309
+ end
310
+
311
+ def do_select(action, parsed, opts = {})
312
+ req_hash = {'TableName' => parsed.table}
313
+ req_hash['AttributesToGet'] = parsed.attrs unless parsed.attrs.empty?
314
+ req_hash['Limit'] = parsed.limit if parsed.limit
315
+ req_hash['ExclusiveStartKey'] = opts[:last_evaluated_key] if opts[:last_evaluated_key]
316
+
317
+ if action == 'Query'
318
+ req_hash['ConsistentRead'] = @consistent if @consistent
319
+ req_hash['IndexName'] = parsed.index if parsed.index
320
+ req_hash['ScanIndexForward'] = parsed.order_asc unless parsed.order_asc.nil?
321
+ end
322
+
323
+ # XXX: req_hash['ReturnConsumedCapacity'] = ...
324
+
325
+ if parsed.count
326
+ req_hash['Select'] = 'COUNT'
327
+ elsif not parsed.attrs.empty?
328
+ req_hash['Select'] = 'SPECIFIC_ATTRIBUTES'
329
+ end
330
+
331
+ # key conditions / scan filter
332
+ if parsed.conds
333
+ param_name = (action == 'Query') ? 'KeyConditions' : 'ScanFilter'
334
+ req_hash[param_name] = {}
335
+
336
+ parsed.conds.each do |key, operator, values|
337
+ h = req_hash[param_name][key] = {
338
+ 'ComparisonOperator' => operator.to_s
339
+ }
340
+
341
+ h['AttributeValueList'] = values.map do |val|
342
+ convert_to_attribute_value(val)
343
+ end
344
+ end
345
+ end # key conditions / scan filter
346
+
347
+ res_data = nil
348
+
349
+ begin
350
+ res_data = @client.query(action, req_hash)
351
+ rescue DynamoDB::Error => e
352
+ if action == 'Query' and e.data['__type'] == 'com.amazon.coral.service#InternalFailure' and not (e.data['message'] || e.data['Message'])
353
+ table_info = (@client.query('DescribeTable', 'TableName' => parsed.table) || {}).fetch('Table', {}) rescue {}
354
+
355
+ unless table_info.fetch('KeySchema', []).any? {|i| i ||= {}; i['KeyType'] == 'RANGE' }
356
+ e.message << 'Query can be performed only on a table with a HASH,RANGE key schema'
357
+ end
358
+ end
359
+
360
+ raise e
361
+ end
362
+
363
+ retval = nil
364
+
365
+ if parsed.count
366
+ retval = res_data['Count']
367
+ else
368
+ retval = res_data['Items'].map {|i| convert_to_ruby_value(i) }
369
+ end
370
+
371
+ if res_data['LastEvaluatedKey']
372
+ @last_action = action
373
+ @last_parsed = parsed
374
+ @last_evaluated_key = res_data['LastEvaluatedKey']
375
+ retval = DynamoDB::Iteratorable.new(retval, res_data['LastEvaluatedKey'])
376
+ else
377
+ @last_action = nil
378
+ @last_parsed = nil
379
+ @last_evaluated_key = nil
380
+ end
381
+
382
+ return retval
383
+ end
384
+
385
+ def do_get(parsed)
386
+ req_hash = {'TableName' => parsed.table}
387
+ req_hash['AttributesToGet'] = parsed.attrs unless parsed.attrs.empty?
388
+ req_hash['ConsistentRead'] = @consistent if @consistent
389
+
390
+ # key
391
+ req_hash['Key'] = {}
392
+
393
+ parsed.conds.each do |key, val|
394
+ req_hash['Key'][key] = convert_to_attribute_value(val)
395
+ end # key
396
+
397
+ convert_to_ruby_value(@client.query('GetItem', req_hash)['Item'])
398
+ end
399
+
400
+ def do_update(parsed)
401
+ req_hash = {
402
+ 'TableName' => parsed.table,
403
+ }
404
+
405
+ # key
406
+ req_hash['Key'] = {}
407
+
408
+ parsed.conds.each do |key, val|
409
+ req_hash['Key'][key] = convert_to_attribute_value(val)
410
+ end # key
411
+
412
+ # attribute updates
413
+ req_hash['AttributeUpdates'] = {}
414
+
415
+ parsed.attrs.each do |attr, val|
416
+ h = req_hash['AttributeUpdates'][attr] = {}
417
+
418
+ if val
419
+ h['Action'] = parsed.action.to_s.upcase
420
+ h['Value'] = convert_to_attribute_value(val)
421
+ else
422
+ h['Action'] = 'DELETE'
423
+ end
424
+ end # attribute updates
425
+
426
+ @client.query('UpdateItem', req_hash)
427
+
428
+ Rownum.new(1)
429
+ end
430
+
431
+ def do_update_all(parsed)
432
+ items = scan_for_update(parsed)
433
+ return Rownum.new(0) if items.empty?
434
+
435
+ n = items.length
436
+
437
+ items.each do |key_hash|
438
+ req_hash = {
439
+ 'TableName' => parsed.table,
440
+ }
441
+
442
+ # key
443
+ req_hash['Key'] = {}
444
+
445
+ key_hash.each do |key, val|
446
+ req_hash['Key'][key] = val
447
+ end # key
448
+
449
+ # attribute updates
450
+ req_hash['AttributeUpdates'] = {}
451
+
452
+ parsed.attrs.each do |attr, val|
453
+ h = req_hash['AttributeUpdates'][attr] = {}
454
+
455
+ if val
456
+ h['Action'] = parsed.action.to_s.upcase
457
+ h['Value'] = convert_to_attribute_value(val)
458
+ else
459
+ h['Action'] = 'DELETE'
460
+ end
461
+ end # attribute updates
462
+
463
+ @client.query('UpdateItem', req_hash)
464
+ end
465
+
466
+ Rownum.new(n)
467
+ end
468
+
469
+ def do_delete(parsed)
470
+ req_hash = {
471
+ 'TableName' => parsed.table,
472
+ }
473
+
474
+ # key
475
+ req_hash['Key'] = {}
476
+
477
+ parsed.conds.each do |key, val|
478
+ req_hash['Key'][key] = convert_to_attribute_value(val)
479
+ end # key
480
+
481
+ @client.query('DeleteItem', req_hash)
482
+
483
+ Rownum.new(1)
484
+ end
485
+
486
+ def do_delete_all(parsed)
487
+ items = scan_for_update(parsed)
488
+ return Rownum.new(0) if items.empty?
489
+
490
+ n = items.length
491
+
492
+ until (chunk = items.slice!(0, MAX_NUMBER_BATCH_PROCESS_ITEMS)).empty?
493
+ operations = []
494
+
495
+ req_hash = {
496
+ 'RequestItems' => {
497
+ parsed.table => operations,
498
+ },
499
+ }
500
+
501
+ chunk.each do |key_hash|
502
+ operations << {
503
+ 'DeleteRequest' => {
504
+ 'Key' => key_hash,
505
+ },
506
+ }
507
+ end
508
+
509
+ @client.query('BatchWriteItem', req_hash)
510
+ end
511
+
512
+ Rownum.new(n)
513
+ end
514
+
515
+ def scan_for_update(parsed)
516
+ # DESCRIBE
517
+ key_names = @client.query('DescribeTable', 'TableName' => parsed.table)['Table']['KeySchema']
518
+ key_names = key_names.map {|h| h['AttributeName'] }
519
+
520
+ items = []
521
+
522
+ # SCAN
523
+ scan = lambda do |last_evaluated_key|
524
+ req_hash = {'TableName' => parsed.table}
525
+ req_hash['AttributesToGet'] = key_names
526
+ req_hash['Limit'] = parsed.limit if parsed.limit
527
+ req_hash['Select'] = 'SPECIFIC_ATTRIBUTES'
528
+ req_hash['ExclusiveStartKey'] = last_evaluated_key if last_evaluated_key
529
+
530
+ # XXX: req_hash['ReturnConsumedCapacity'] = ...
531
+
532
+ # scan filter
533
+ if parsed.conds
534
+ req_hash['ScanFilter'] = {}
535
+
536
+ parsed.conds.each do |key, operator, values|
537
+ h = req_hash['ScanFilter'][key] = {
538
+ 'ComparisonOperator' => operator.to_s
539
+ }
540
+
541
+ h['AttributeValueList'] = values.map do |val|
542
+ convert_to_attribute_value(val)
543
+ end
544
+ end
545
+ end # scan filter
546
+
547
+ res_data = @client.query('Scan', req_hash)
548
+ items.concat(res_data['Items'])
549
+ res_data['LastEvaluatedKey']
550
+ end
551
+
552
+ lek = nil
553
+
554
+ loop do
555
+ lek = scan.call(lek)
556
+ break unless lek
557
+ end
558
+
559
+ return items
560
+ end
561
+
562
+ def convert_to_attribute_value(val)
563
+ suffix = ''
564
+ obj = val
565
+
566
+ if val.kind_of?(Array)
567
+ suffix = 'S'
568
+ obj = val.first
569
+ val = val.map {|i| i.to_s }
570
+ else
571
+ val = val.to_s
572
+ end
573
+
574
+ case obj
575
+ when DynamoDB::Binary
576
+ {"B#{suffix}" => val}
577
+ when String
578
+ {"S#{suffix}" => val}
579
+ when Numeric
580
+ {"N#{suffix}" => val}
581
+ else
582
+ raise 'must not happen'
583
+ end
584
+ end
585
+
586
+ def convert_to_ruby_value(item)
587
+ h = {}
588
+
589
+ (item || {}).sort_by {|a, b| a }.map do |name, val|
590
+ val = val.map do |val_type, ddb_val|
591
+ case val_type
592
+ when 'NS'
593
+ ddb_val.map {|i| str_to_num(i) }
594
+ when 'N'
595
+ str_to_num(ddb_val)
596
+ else
597
+ ddb_val
598
+ end
599
+ end
600
+
601
+ val = val.first if val.length == 1
602
+ h[name] = val
603
+ end
604
+
605
+ return h
606
+ end
607
+
608
+ def do_insert(parsed)
609
+ n = 0
610
+
611
+ until (chunk = parsed.values.slice!(0, MAX_NUMBER_BATCH_PROCESS_ITEMS)).empty?
612
+ operations = []
613
+
614
+ req_hash = {
615
+ 'RequestItems' => {
616
+ parsed.table => operations,
617
+ },
618
+ }
619
+
620
+ chunk.each do |val_list|
621
+ h = {}
622
+
623
+ operations << {
624
+ 'PutRequest' => {
625
+ 'Item' => h,
626
+ },
627
+ }
628
+
629
+ parsed.attrs.zip(val_list).each do |name, val|
630
+ h[name] = convert_to_attribute_value(val)
631
+ end
632
+ end
633
+
634
+ @client.query('BatchWriteItem', req_hash)
635
+ n += chunk.length
636
+ end
637
+
638
+ Rownum.new(n)
639
+ end
640
+
641
+ def str_to_num(str)
642
+ str =~ /\./ ? str.to_f : str.to_i
643
+ end
644
+
645
+ end # Driver
646
+ end # DynamoDB