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.
- data/README +148 -0
- data/bin/ddbcli +66 -0
- data/lib/ddbcli.rb +13 -0
- data/lib/ddbcli/cli/evaluate.rb +51 -0
- data/lib/ddbcli/cli/functions.rb +160 -0
- data/lib/ddbcli/cli/help.rb +116 -0
- data/lib/ddbcli/cli/options.rb +45 -0
- data/lib/ddbcli/ddb-binary.rb +15 -0
- data/lib/ddbcli/ddb-client.rb +239 -0
- data/lib/ddbcli/ddb-driver.rb +646 -0
- data/lib/ddbcli/ddb-endpoint.rb +31 -0
- data/lib/ddbcli/ddb-error.rb +10 -0
- data/lib/ddbcli/ddb-iteratorable.rb +11 -0
- data/lib/ddbcli/ddb-parser.tab.rb +1383 -0
- data/lib/ddbcli/ddb-parser.y +598 -0
- data/lib/ddbcli/ddb-rubyext.rb +43 -0
- metadata +77 -0
|
@@ -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,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
|