td 0.11.8.2 → 0.11.9
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 +4 -4
- data/ChangeLog +18 -9
- data/Gemfile +2 -0
- data/lib/td/command/connector.rb +299 -0
- data/lib/td/command/job.rb +80 -87
- data/lib/td/command/list.rb +16 -1
- data/lib/td/command/runner.rb +1 -0
- data/lib/td/command/table.rb +15 -4
- data/lib/td/command/user.rb +1 -1
- data/lib/td/updater.rb +26 -26
- data/lib/td/version.rb +1 -1
- data/spec/td/command/job_spec.rb +208 -12
- data/spec/td/{command_sched_spec.rb → command/sched_spec.rb} +0 -0
- data/spec/td/command/table_spec.rb +186 -0
- data/spec/td/updater_spec.rb +37 -1
- data/td.gemspec +1 -1
- metadata +9 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 35121e210f89e8af8d88f3de09c5da1a46f465f5
|
4
|
+
data.tar.gz: 2ed8a007214c1861d9653ad88a6e204b11959487
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e29254a97c4170455119d3d0d11a8c026d05b431f3fe58ad69bbaeb7f6837805fcdbe51fdd91c8ebe3ae6a9c7077f3312cd681a0f865a3bf2eddb43399446be0
|
7
|
+
data.tar.gz: 908f511b6ed2f6b293cdc809b203a367869607f7fd0d130c5f85919cc5d0462fec372a214c31df2ecde30cde248804c173fb9a5d32136d84f605d5557fc09cb3
|
data/ChangeLog
CHANGED
@@ -1,12 +1,21 @@
|
|
1
|
-
== 2015-
|
2
|
-
|
3
|
-
*
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
1
|
+
== 2015-04-17 version 0.11.9
|
2
|
+
|
3
|
+
* add connector commands.
|
4
|
+
* add tsv.gz format to export command.
|
5
|
+
* add --null option to job:show that specifies what character to represent
|
6
|
+
null value in csv or tsv format. It emits 'null' by default for backward
|
7
|
+
compatibility but you can specify '--null ""' to use empty string instead
|
8
|
+
of 'null'.
|
9
|
+
* fix: remove illegal TAB character at the end of header record. Thanks y-ken!
|
10
|
+
* fix: do fancy format of numbers in table:list only if format is 'table'.
|
11
|
+
* fix: job:show to convert NaN to empty String if format is csv or tsv. The
|
12
|
+
column data NaN, Infinity and -Infinity caused an error if previous
|
13
|
+
versions. If json format is specified it still raises an error because JSON
|
14
|
+
format does not have representation of those values.
|
15
|
+
* fix: internal cwd handling for auto jar update that caused import:upload to
|
16
|
+
fail by changing cwd before reading specified file.
|
17
|
+
* fix: table:list to consider import_only databases. It just skips such
|
18
|
+
databases and no longer crashes with insufficient permission.
|
10
19
|
|
11
20
|
== 2015-02-20 version 0.11.8
|
12
21
|
|
data/Gemfile
CHANGED
@@ -0,0 +1,299 @@
|
|
1
|
+
require 'td/command/common'
|
2
|
+
require 'td/command/job'
|
3
|
+
require 'json'
|
4
|
+
require 'uri'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
module TreasureData
|
8
|
+
module Command
|
9
|
+
|
10
|
+
def required(opt, value)
|
11
|
+
if value.nil?
|
12
|
+
raise ParameterConfigurationError, "#{opt} option required"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def connector_guess(op)
|
17
|
+
type = 's3'
|
18
|
+
id = secret = source = nil
|
19
|
+
out = 'td-bulkload.yml'
|
20
|
+
|
21
|
+
op.on('--type[=TYPE]', "connector type; only 's3' is supported") { |s| type = s }
|
22
|
+
op.on('--access-id ID', "access ID (S3 access key id for type: s3)") { |s| id = s }
|
23
|
+
op.on('--access-secret SECRET', "access secret (S3 secret access key for type: s3)") { |s| secret = s }
|
24
|
+
op.on('--source SOURCE', "resource(s) URI to be imported (e.g. https://s3-us-west-1.amazonaws.com/bucketname/path/prefix/to/import/)") { |s| source = s }
|
25
|
+
op.on('--out FILE_NAME', "configuration file") { |s| out = s }
|
26
|
+
|
27
|
+
config = op.cmd_parse
|
28
|
+
if config
|
29
|
+
job = prepare_bulkload_job_config(config)
|
30
|
+
out ||= config
|
31
|
+
else
|
32
|
+
required('--access-id', id)
|
33
|
+
required('--access-secret', secret)
|
34
|
+
required('--source', source)
|
35
|
+
required('--out', out)
|
36
|
+
|
37
|
+
uri = URI.parse(source)
|
38
|
+
endpoint = uri.host
|
39
|
+
path_components = uri.path.scan(/\/[^\/]*/)
|
40
|
+
bucket = path_components.shift.sub(/\//, '')
|
41
|
+
path_prefix = path_components.join.sub(/\//, '')
|
42
|
+
|
43
|
+
job = API::BulkLoad::BulkLoad.from_hash(
|
44
|
+
:config => {
|
45
|
+
:type => type,
|
46
|
+
:access_key_id => id,
|
47
|
+
:secret_access_key => secret,
|
48
|
+
:endpoint => endpoint,
|
49
|
+
:bucket => bucket,
|
50
|
+
:path_prefix => path_prefix,
|
51
|
+
}
|
52
|
+
).validate
|
53
|
+
end
|
54
|
+
|
55
|
+
client = get_client
|
56
|
+
job = client.bulk_load_guess(job)
|
57
|
+
|
58
|
+
create_bulkload_job_file_backup(out)
|
59
|
+
if /\.json\z/ =~ out
|
60
|
+
config_str = JSON.pretty_generate(job.to_h)
|
61
|
+
else
|
62
|
+
config_str = YAML.dump(job.to_h)
|
63
|
+
end
|
64
|
+
File.open(out, 'w') do |f|
|
65
|
+
f << config_str
|
66
|
+
end
|
67
|
+
|
68
|
+
puts "Created #{out} file."
|
69
|
+
puts "Use '#{$prog} " + Config.cl_options_string + "connector:preview #{out}' to see bulk load preview."
|
70
|
+
end
|
71
|
+
|
72
|
+
def connector_preview(op)
|
73
|
+
set_render_format_option(op)
|
74
|
+
config_file = op.cmd_parse
|
75
|
+
job = prepare_bulkload_job_config(config_file)
|
76
|
+
client = get_client()
|
77
|
+
preview = client.bulk_load_preview(job)
|
78
|
+
|
79
|
+
cols = preview.schema.sort_by { |col|
|
80
|
+
col['index']
|
81
|
+
}
|
82
|
+
fields = cols.map { |col| col['name'] + ':' + col['type'] }
|
83
|
+
types = cols.map { |col| col['type'] }
|
84
|
+
rows = preview.records.map { |row|
|
85
|
+
cols = {}
|
86
|
+
row.each_with_index do |col, idx|
|
87
|
+
cols[fields[idx]] = col.inspect
|
88
|
+
end
|
89
|
+
cols
|
90
|
+
}
|
91
|
+
|
92
|
+
puts cmd_render_table(rows, :fields => fields, :render_format => op.render_format)
|
93
|
+
|
94
|
+
puts "Update #{config_file} and use '#{$prog} " + Config.cl_options_string + "connector:preview #{config_file}' to preview again."
|
95
|
+
puts "Use '#{$prog} " + Config.cl_options_string + "connector:issue #{config_file}' to run Server-side bulk load."
|
96
|
+
end
|
97
|
+
|
98
|
+
def connector_issue(op)
|
99
|
+
database = table = nil
|
100
|
+
time_column = nil
|
101
|
+
wait = exclude = false
|
102
|
+
op.on('--database DB_NAME', "destination database") { |s| database = s }
|
103
|
+
op.on('--table TABLE_NAME', "destination table") { |s| table = s }
|
104
|
+
op.on('--time-column COLUMN_NAME', "data partitioning key") { |s| time_column = s }
|
105
|
+
op.on('-w', '--wait', 'wait for finishing the job', TrueClass) { |b| wait = b }
|
106
|
+
op.on('-x', '--exclude', 'do not automatically retrieve the job result', TrueClass) { |b| exclude = b }
|
107
|
+
|
108
|
+
config_file = op.cmd_parse
|
109
|
+
|
110
|
+
required('--database', database)
|
111
|
+
required('--table', table)
|
112
|
+
|
113
|
+
job = prepare_bulkload_job_config(config_file)
|
114
|
+
job['time_column'] = time_column if time_column
|
115
|
+
|
116
|
+
client = get_client()
|
117
|
+
job_id = client.bulk_load_issue(database, table, job)
|
118
|
+
|
119
|
+
puts "Job #{job_id} is queued."
|
120
|
+
puts "Use '#{$prog} " + Config.cl_options_string + "job:show #{job_id}' to show the status."
|
121
|
+
|
122
|
+
if wait
|
123
|
+
wait_connector_job(client, job_id, exclude)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def connector_list(op)
|
128
|
+
set_render_format_option(op)
|
129
|
+
op.cmd_parse
|
130
|
+
|
131
|
+
client = get_client()
|
132
|
+
# TODO database and table is empty at present. Fix API or Client.
|
133
|
+
keys = ['name', 'cron', 'timezone', 'delay', 'database', 'table', 'config']
|
134
|
+
fields = keys.map { |e| e.capitalize.to_sym }
|
135
|
+
rows = client.bulk_load_list().sort_by { |e|
|
136
|
+
e['name']
|
137
|
+
}.map { |e|
|
138
|
+
Hash[fields.zip(e.to_h.values_at(*keys))]
|
139
|
+
}
|
140
|
+
|
141
|
+
puts cmd_render_table(rows, :fields => fields, :render_format => op.render_format)
|
142
|
+
end
|
143
|
+
|
144
|
+
def connector_create(op)
|
145
|
+
# TODO it's a must parameter at this moment but API should be fixed
|
146
|
+
opts = {:timezone => 'UTC'}
|
147
|
+
op.on('--time-column COLUMN_NAME', "data partitioning key") {|s|
|
148
|
+
opts[:time_column] = s
|
149
|
+
}
|
150
|
+
op.on('-t', '--timezone TZ', "name of the timezone.",
|
151
|
+
" Only extended timezones like 'Asia/Tokyo', 'America/Los_Angeles' are supported,",
|
152
|
+
" (no 'PST', 'PDT', etc...).",
|
153
|
+
" When a timezone is specified, the cron schedule is referred to that timezone.",
|
154
|
+
" Otherwise, the cron schedule is referred to the UTC timezone.",
|
155
|
+
" E.g. cron schedule '0 12 * * *' will execute daily at 5 AM without timezone option",
|
156
|
+
" and at 12PM with the -t / --timezone 'America/Los_Angeles' timezone option") {|s|
|
157
|
+
opts[:timezone] = s
|
158
|
+
}
|
159
|
+
op.on('-D', '--delay SECONDS', 'delay time of the schedule', Integer) {|i|
|
160
|
+
opts[:delay] = i
|
161
|
+
}
|
162
|
+
|
163
|
+
name, cron, database, table, config_file = op.cmd_parse
|
164
|
+
|
165
|
+
job = prepare_bulkload_job_config(config_file)
|
166
|
+
opts[:cron] = cron
|
167
|
+
|
168
|
+
client = get_client()
|
169
|
+
get_table(client, database, table)
|
170
|
+
|
171
|
+
session = client.bulk_load_create(name, database, table, job, opts)
|
172
|
+
dump_connector_session(session)
|
173
|
+
end
|
174
|
+
|
175
|
+
def connector_show(op)
|
176
|
+
name = op.cmd_parse
|
177
|
+
|
178
|
+
client = get_client()
|
179
|
+
session = client.bulk_load_show(name)
|
180
|
+
dump_connector_session(session)
|
181
|
+
end
|
182
|
+
|
183
|
+
def connector_update(op)
|
184
|
+
name, config_file = op.cmd_parse
|
185
|
+
|
186
|
+
job = prepare_bulkload_job_config(config_file)
|
187
|
+
|
188
|
+
client = get_client()
|
189
|
+
session = client.bulk_load_update(name, job)
|
190
|
+
dump_connector_session(session)
|
191
|
+
end
|
192
|
+
|
193
|
+
def connector_delete(op)
|
194
|
+
name = op.cmd_parse
|
195
|
+
|
196
|
+
client = get_client()
|
197
|
+
session = client.bulk_load_delete(name)
|
198
|
+
puts 'Deleted session'
|
199
|
+
puts '--'
|
200
|
+
dump_connector_session(session)
|
201
|
+
end
|
202
|
+
|
203
|
+
def connector_history(op)
|
204
|
+
set_render_format_option(op)
|
205
|
+
name = op.cmd_parse
|
206
|
+
|
207
|
+
fields = [:JobID, :Status, :Records, :Database, :Table, :Priority, :Started, :Duration]
|
208
|
+
client = get_client()
|
209
|
+
rows = client.bulk_load_history(name).map { |e|
|
210
|
+
{
|
211
|
+
:JobID => e.job_id,
|
212
|
+
:Status => e.status,
|
213
|
+
:Records => e.records,
|
214
|
+
# TODO: td-client-ruby should retuan only name
|
215
|
+
:Database => e.database['name'],
|
216
|
+
:Table => e.table['name'],
|
217
|
+
:Priority => e.priority,
|
218
|
+
:Started => Time.at(e.start_at),
|
219
|
+
:Duration => (e.end_at.nil? ? Time.now.to_i : e.end_at) - e.start_at,
|
220
|
+
}
|
221
|
+
}
|
222
|
+
puts cmd_render_table(rows, :fields => fields, :render_format => op.render_format)
|
223
|
+
end
|
224
|
+
|
225
|
+
def connector_run(op)
|
226
|
+
wait = exclude = false
|
227
|
+
op.on('-w', '--wait', 'wait for finishing the job', TrueClass) { |b| wait = b }
|
228
|
+
op.on('-x', '--exclude', 'do not automatically retrieve the job result', TrueClass) { |b| exclude = b }
|
229
|
+
|
230
|
+
name, scheduled_time = op.cmd_parse
|
231
|
+
|
232
|
+
client = get_client()
|
233
|
+
job_id = client.bulk_load_run(name)
|
234
|
+
puts "Job #{job_id} is queued."
|
235
|
+
puts "Use '#{$prog} " + Config.cl_options_string + "job:show #{job_id}' to show the status."
|
236
|
+
|
237
|
+
if wait
|
238
|
+
wait_connector_job(client, job_id, exclude)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
private
|
243
|
+
|
244
|
+
def file_type(str)
|
245
|
+
begin
|
246
|
+
YAML.load(str)
|
247
|
+
return :yaml
|
248
|
+
rescue
|
249
|
+
end
|
250
|
+
begin
|
251
|
+
JSON.parse(str)
|
252
|
+
return :json
|
253
|
+
rescue
|
254
|
+
end
|
255
|
+
nil
|
256
|
+
end
|
257
|
+
|
258
|
+
def prepare_bulkload_job_config(config_file)
|
259
|
+
unless File.exist?(config_file)
|
260
|
+
raise ParameterConfigurationError, "configuration file: #{config_file} not found"
|
261
|
+
end
|
262
|
+
config_str = File.read(config_file)
|
263
|
+
if file_type(config_str) == :yaml
|
264
|
+
config_str = JSON.pretty_generate(YAML.load(config_str))
|
265
|
+
end
|
266
|
+
API::BulkLoad::BulkLoad.from_json(config_str)
|
267
|
+
end
|
268
|
+
|
269
|
+
def create_bulkload_job_file_backup(out)
|
270
|
+
return unless File.exist?(out)
|
271
|
+
0.upto(100) do |idx|
|
272
|
+
backup = "#{out}.#{idx}"
|
273
|
+
unless File.exist?(backup)
|
274
|
+
FileUtils.mv(out, backup)
|
275
|
+
return
|
276
|
+
end
|
277
|
+
end
|
278
|
+
raise "backup file creation failed"
|
279
|
+
end
|
280
|
+
|
281
|
+
def dump_connector_session(session)
|
282
|
+
puts "Name : #{session.name}"
|
283
|
+
puts "Cron : #{session.cron}"
|
284
|
+
puts "Timezone : #{session.timezone}"
|
285
|
+
puts "Delay : #{session.delay}"
|
286
|
+
puts "Database : #{session.database}"
|
287
|
+
puts "Table : #{session.table}"
|
288
|
+
puts "Config"
|
289
|
+
puts YAML.dump(session.config.to_h)
|
290
|
+
end
|
291
|
+
|
292
|
+
def wait_connector_job(client, job_id, exclude)
|
293
|
+
job = client.job(job_id)
|
294
|
+
wait_job(job, true)
|
295
|
+
puts "Status : #{job.status}"
|
296
|
+
end
|
297
|
+
|
298
|
+
end
|
299
|
+
end
|
data/lib/td/command/job.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
module TreasureData
|
3
2
|
module Command
|
4
3
|
|
@@ -143,6 +142,10 @@ module Command
|
|
143
142
|
exclude = b
|
144
143
|
}
|
145
144
|
|
145
|
+
op.on('--null STRING', "null expression in csv or tsv") {|s|
|
146
|
+
render_opts[:null_expr] = s.to_s
|
147
|
+
}
|
148
|
+
|
146
149
|
job_id = op.cmd_parse
|
147
150
|
|
148
151
|
# parameter concurrency validation
|
@@ -166,8 +169,38 @@ module Command
|
|
166
169
|
"Option -l / --limit is only valid when not outputting to file (no -o / --output option provided)"
|
167
170
|
end
|
168
171
|
|
172
|
+
get_and_show_result(job_id, wait, exclude, output, limit, format, render_opts, verbose)
|
173
|
+
end
|
174
|
+
|
175
|
+
def job_status(op)
|
176
|
+
job_id = op.cmd_parse
|
169
177
|
client = get_client
|
170
178
|
|
179
|
+
puts client.job_status(job_id)
|
180
|
+
end
|
181
|
+
|
182
|
+
def job_kill(op)
|
183
|
+
job_id = op.cmd_parse
|
184
|
+
|
185
|
+
client = get_client
|
186
|
+
|
187
|
+
former_status = client.kill(job_id)
|
188
|
+
if TreasureData::Job::FINISHED_STATUS.include?(former_status)
|
189
|
+
$stderr.puts "Job #{job_id} is already finished (#{former_status})"
|
190
|
+
exit 0
|
191
|
+
end
|
192
|
+
|
193
|
+
if former_status == TreasureData::Job::STATUS_RUNNING
|
194
|
+
$stderr.puts "Job #{job_id} is killed."
|
195
|
+
else
|
196
|
+
$stderr.puts "Job #{job_id} is canceled."
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
private
|
201
|
+
|
202
|
+
def get_and_show_result(job_id, wait, exclude = false, output = nil, limit = nil, format = nil, render_opts = {}, verbose = false)
|
203
|
+
client = get_client
|
171
204
|
job = client.job(job_id)
|
172
205
|
|
173
206
|
puts "JobID : #{job.job_id}"
|
@@ -194,61 +227,14 @@ module Command
|
|
194
227
|
end
|
195
228
|
end
|
196
229
|
|
197
|
-
# up to 7 retries with exponential (base 2) back-off starting at 'retry_delay'
|
198
|
-
retry_delay = 5
|
199
|
-
max_cumul_retry_delay = 200
|
200
|
-
cumul_retry_delay = 0
|
201
|
-
|
202
230
|
if wait && !job.finished?
|
203
231
|
wait_job(job)
|
204
232
|
if [:hive, :pig, :impala, :presto].include?(job.type) && !exclude
|
205
|
-
|
206
|
-
|
207
|
-
begin
|
208
|
-
show_result(job, output, limit, format, render_opts)
|
209
|
-
rescue TreasureData::NotFoundError => e
|
210
|
-
# Got 404 because result not found.
|
211
|
-
rescue TreasureData::APIError, # HTTP status code 500 or more
|
212
|
-
Errno::ECONNREFUSED, Errno::ECONNRESET, Timeout::Error, EOFError,
|
213
|
-
OpenSSL::SSL::SSLError, SocketError => e
|
214
|
-
# don't retry on 300 and 400 errors
|
215
|
-
if e.class == TreasureData::APIError && e.message !~ /^5\d\d:\s+/
|
216
|
-
raise e
|
217
|
-
end
|
218
|
-
if cumul_retry_delay > max_cumul_retry_delay
|
219
|
-
raise e
|
220
|
-
end
|
221
|
-
$stderr.puts "Error #{e.class}: #{e.message}. Retrying after #{retry_delay} seconds..."
|
222
|
-
sleep retry_delay
|
223
|
-
cumul_retry_delay += retry_delay
|
224
|
-
retry_delay *= 2
|
225
|
-
retry
|
226
|
-
end
|
233
|
+
show_result_with_retry(job, output, limit, format, render_opts)
|
227
234
|
end
|
228
|
-
|
229
235
|
else
|
230
236
|
if [:hive, :pig, :impala, :presto].include?(job.type) && !exclude && job.finished?
|
231
|
-
|
232
|
-
begin
|
233
|
-
show_result(job, output, limit, format, render_opts)
|
234
|
-
rescue TreasureData::NotFoundError => e
|
235
|
-
# Got 404 because result not found.
|
236
|
-
rescue TreasureData::APIError,
|
237
|
-
Errno::ECONNREFUSED, Errno::ECONNRESET, Timeout::Error, EOFError,
|
238
|
-
OpenSSL::SSL::SSLError, SocketError => e
|
239
|
-
# don't retry on 300 and 400 errors
|
240
|
-
if e.class == TreasureData::APIError && e.message !~ /^5\d\d:\s+/
|
241
|
-
raise e
|
242
|
-
end
|
243
|
-
if cumul_retry_delay > max_cumul_retry_delay
|
244
|
-
raise e
|
245
|
-
end
|
246
|
-
$stderr.puts "Error #{e.class}: #{e.message}. Retrying after #{retry_delay} seconds..."
|
247
|
-
sleep retry_delay
|
248
|
-
cumul_retry_delay += retry_delay
|
249
|
-
retry_delay *= 2
|
250
|
-
retry
|
251
|
-
end
|
237
|
+
show_result_with_retry(job, output, limit, format, render_opts)
|
252
238
|
end
|
253
239
|
|
254
240
|
if verbose
|
@@ -272,32 +258,6 @@ module Command
|
|
272
258
|
puts "\rUse '-v' option to show detailed messages." + " " * 20 unless verbose
|
273
259
|
end
|
274
260
|
|
275
|
-
def job_status(op)
|
276
|
-
job_id = op.cmd_parse
|
277
|
-
client = get_client
|
278
|
-
|
279
|
-
puts client.job_status(job_id)
|
280
|
-
end
|
281
|
-
|
282
|
-
def job_kill(op)
|
283
|
-
job_id = op.cmd_parse
|
284
|
-
|
285
|
-
client = get_client
|
286
|
-
|
287
|
-
former_status = client.kill(job_id)
|
288
|
-
if TreasureData::Job::FINISHED_STATUS.include?(former_status)
|
289
|
-
$stderr.puts "Job #{job_id} is already finished (#{former_status})"
|
290
|
-
exit 0
|
291
|
-
end
|
292
|
-
|
293
|
-
if former_status == TreasureData::Job::STATUS_RUNNING
|
294
|
-
$stderr.puts "Job #{job_id} is killed."
|
295
|
-
else
|
296
|
-
$stderr.puts "Job #{job_id} is canceled."
|
297
|
-
end
|
298
|
-
end
|
299
|
-
|
300
|
-
private
|
301
261
|
def wait_job(job, first_call = false)
|
302
262
|
$stderr.puts "queued..."
|
303
263
|
|
@@ -328,6 +288,35 @@ module Command
|
|
328
288
|
end
|
329
289
|
end
|
330
290
|
|
291
|
+
def show_result_with_retry(job, output, limit, format, render_opts)
|
292
|
+
# up to 7 retries with exponential (base 2) back-off starting at 'retry_delay'
|
293
|
+
retry_delay = 5
|
294
|
+
max_cumul_retry_delay = 200
|
295
|
+
cumul_retry_delay = 0
|
296
|
+
|
297
|
+
puts "Result :"
|
298
|
+
begin
|
299
|
+
show_result(job, output, limit, format, render_opts)
|
300
|
+
rescue TreasureData::NotFoundError => e
|
301
|
+
# Got 404 because result not found.
|
302
|
+
rescue TreasureData::APIError, # HTTP status code 500 or more
|
303
|
+
Errno::ECONNREFUSED, Errno::ECONNRESET, Timeout::Error, EOFError,
|
304
|
+
OpenSSL::SSL::SSLError, SocketError => e
|
305
|
+
# don't retry on 300 and 400 errors
|
306
|
+
if e.class == TreasureData::APIError && e.message !~ /^5\d\d:\s+/
|
307
|
+
raise e
|
308
|
+
end
|
309
|
+
if cumul_retry_delay > max_cumul_retry_delay
|
310
|
+
raise e
|
311
|
+
end
|
312
|
+
$stderr.puts "Error #{e.class}: #{e.message}. Retrying after #{retry_delay} seconds..."
|
313
|
+
sleep retry_delay
|
314
|
+
cumul_retry_delay += retry_delay
|
315
|
+
retry_delay *= 2
|
316
|
+
retry
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
331
320
|
def show_result(job, output, limit, format, render_opts={})
|
332
321
|
if output
|
333
322
|
write_result(job, output, limit, format, render_opts)
|
@@ -387,7 +376,7 @@ module Command
|
|
387
376
|
job.result_each_with_compr_size {|row, compr_size|
|
388
377
|
# TODO limit the # of columns
|
389
378
|
writer << row.map {|col|
|
390
|
-
dump_column(col)
|
379
|
+
dump_column(col, render_opts[:null_expr])
|
391
380
|
}
|
392
381
|
n_rows += 1
|
393
382
|
if n_rows % 100 == 0 # flush every 100 recods
|
@@ -405,10 +394,7 @@ module Command
|
|
405
394
|
open_file(output, "w") {|f|
|
406
395
|
# output headers
|
407
396
|
if render_opts[:header] && job.hive_result_schema
|
408
|
-
job.hive_result_schema.
|
409
|
-
f.write name + "\t"
|
410
|
-
}
|
411
|
-
f.write "\n"
|
397
|
+
f.write job.hive_result_schema.map {|name, type| name}.join("\t") + "\n"
|
412
398
|
end
|
413
399
|
# output data
|
414
400
|
n_rows = 0
|
@@ -417,8 +403,9 @@ module Command
|
|
417
403
|
"NOTE: the job result is being written to #{output} in tsv format",
|
418
404
|
job.result_size, 0.1, 1)
|
419
405
|
end
|
406
|
+
|
420
407
|
job.result_each_with_compr_size {|row, compr_size|
|
421
|
-
f.write row.map {|col| dump_column(col)}.join("\t") + "\n"
|
408
|
+
f.write row.map {|col| dump_column(col, render_opts[:null_expr])}.join("\t") + "\n"
|
422
409
|
n_rows += 1
|
423
410
|
if n_rows % 100 == 0
|
424
411
|
f.flush # flush every 100 recods
|
@@ -493,7 +480,7 @@ module Command
|
|
493
480
|
job.result_each_with_compr_size {|row, compr_size|
|
494
481
|
indicator.update(compr_size)
|
495
482
|
rows << row.map {|v|
|
496
|
-
dump_column_safe_utf8(v)
|
483
|
+
dump_column_safe_utf8(v, render_opts[:null_expr])
|
497
484
|
}
|
498
485
|
n_rows += 1
|
499
486
|
break if !limit.nil? and n_rows == limit
|
@@ -514,15 +501,17 @@ module Command
|
|
514
501
|
end
|
515
502
|
end
|
516
503
|
|
517
|
-
def dump_column(v)
|
518
|
-
|
504
|
+
def dump_column(v, null_expr = nil)
|
505
|
+
v = null_expr if v.nil? && null_expr
|
506
|
+
|
507
|
+
s = v.is_a?(String) ? v.to_s : Yajl.dump(sanitize_infinite_value(v))
|
519
508
|
# CAUTION: msgpack-ruby populates byte sequences as Encoding.default_internal which should be BINARY
|
520
509
|
s = s.force_encoding('BINARY') if s.respond_to?(:encode)
|
521
510
|
s
|
522
511
|
end
|
523
512
|
|
524
|
-
def dump_column_safe_utf8(v)
|
525
|
-
s = dump_column(v)
|
513
|
+
def dump_column_safe_utf8(v, null_expr = false)
|
514
|
+
s = dump_column(v, null_expr)
|
526
515
|
# Here does UTF-8 -> UTF-16LE -> UTF8 conversion:
|
527
516
|
# a) to make sure the string doesn't include invalid byte sequence
|
528
517
|
# b) to display multi-byte characters as it is
|
@@ -532,6 +521,10 @@ module Command
|
|
532
521
|
s
|
533
522
|
end
|
534
523
|
|
524
|
+
def sanitize_infinite_value(v)
|
525
|
+
(v.is_a?(Float) && !v.finite?) ? v.to_s : v
|
526
|
+
end
|
527
|
+
|
535
528
|
def job_priority_name_of(id)
|
536
529
|
PRIORITY_FORMAT_MAP[id] || 'NORMAL'
|
537
530
|
end
|
data/lib/td/command/list.rb
CHANGED
@@ -311,7 +311,7 @@ module List
|
|
311
311
|
# TODO acl:test
|
312
312
|
|
313
313
|
add_list 'server:status', %w[], 'Show status of the Treasure Data server'
|
314
|
-
add_list 'server:endpoint', %w[api_endpoint], "Set the Treasure Data API server's endpoint (must be a valid URI)", ["
|
314
|
+
add_list 'server:endpoint', %w[api_endpoint], "Set the Treasure Data API server's endpoint (must be a valid URI)", ["server:endpoint 'https://api.treasuredata.com'"]
|
315
315
|
|
316
316
|
add_list 'sample:apache', %w[path.json], 'Create a sample log file', [], false
|
317
317
|
|
@@ -320,6 +320,19 @@ module List
|
|
320
320
|
|
321
321
|
add_list 'update', %w[], 'Update td and related libraries for TreasureData toolbelt'
|
322
322
|
|
323
|
+
add_list 'connector:guess', %w[config?], 'Run guess to generate connector config file', ['connector:guess td-bulkload.yml', 'connector:guess --access-id s3accessId --access-secret s3AccessKey --source https://s3.amazonaws.com/bucketname/path/prefix --database connector_database --table connector_table']
|
324
|
+
add_list 'connector:preview', %w[config], 'Show preview of connector execution', ['connector:preview td-bulkload.yml']
|
325
|
+
|
326
|
+
add_list 'connector:issue', %w[config], 'Run one time connector execution', ['connector:issue td-bulkload.yml']
|
327
|
+
|
328
|
+
add_list 'connector:list', %w[], 'Show list of connector sessions', ['connector:list']
|
329
|
+
add_list 'connector:create', %w[name cron database table config], 'Create new connector session', ['connector:create connector1 "0 * * * *" connector_database connector_table td-bulkload.yml']
|
330
|
+
add_list 'connector:show', %w[name], 'Show connector session', ['connector:show connector1']
|
331
|
+
add_list 'connector:update', %w[name config], 'Modify connector session', ['connector:update connector1 td-bulkload.yml']
|
332
|
+
add_list 'connector:delete', %w[name], 'Delete connector session', ['connector:delete connector1']
|
333
|
+
add_list 'connector:history', %w[name], 'Show job history of connector session', ['connector:history connector1']
|
334
|
+
add_list 'connector:run', %w[name time], 'Run connector session for the specified time', ['connector:run connector1 "2016-01-01 00:00:00"']
|
335
|
+
|
323
336
|
# aliases
|
324
337
|
add_alias 'db', 'db:show'
|
325
338
|
add_alias 'dbs', 'db:list'
|
@@ -377,6 +390,8 @@ module List
|
|
377
390
|
|
378
391
|
add_alias 's', 'status'
|
379
392
|
|
393
|
+
add_alias 'connector', 'connector:guess'
|
394
|
+
|
380
395
|
# backward compatibility
|
381
396
|
add_alias 'show-databases', 'db:list'
|
382
397
|
add_alias 'show-dbs', 'db:list'
|
data/lib/td/command/runner.rb
CHANGED
data/lib/td/command/table.rb
CHANGED
@@ -144,18 +144,25 @@ module Command
|
|
144
144
|
databases = client.databases
|
145
145
|
end
|
146
146
|
|
147
|
-
has_item = databases.select {|db|
|
147
|
+
has_item = databases.select {|db|
|
148
|
+
db.permission != :import_only ? (db.tables.select {|table| table.type == :item}.length > 0) : false
|
149
|
+
}.length > 0
|
148
150
|
|
151
|
+
# ref. https://github.com/treasure-data/td/issues/26
|
152
|
+
should_number_format = [nil, "table"].include?(op.render_format)
|
149
153
|
rows = []
|
150
154
|
::Parallel.each(databases, :in_threads => num_threads) {|db|
|
151
155
|
begin
|
156
|
+
if db.permission == :import_only
|
157
|
+
next
|
158
|
+
end
|
152
159
|
db.tables.each {}
|
153
160
|
db.tables.each {|table|
|
154
161
|
pschema = table.schema.fields.map {|f|
|
155
162
|
"#{f.name}:#{f.type}"
|
156
163
|
}.join(', ')
|
157
164
|
new_row = {
|
158
|
-
:Database => db.name, :Table => table.name, :Type => table.type.to_s, :Count => TreasureData::Helpers.format_with_delimiter(table.count),
|
165
|
+
:Database => db.name, :Table => table.name, :Type => table.type.to_s, :Count => (should_number_format ? TreasureData::Helpers.format_with_delimiter(table.count) : table.count),
|
159
166
|
:Size => show_size_in_bytes ? TreasureData::Helpers.format_with_delimiter(table.estimated_storage_size) : table.estimated_storage_size_string,
|
160
167
|
'Last import' => table.last_import ? table.last_import.localtime : nil,
|
161
168
|
'Last log timestamp' => table.last_log_timestamp ? table.last_log_timestamp.localtime : nil,
|
@@ -188,8 +195,12 @@ module Command
|
|
188
195
|
|
189
196
|
if rows.empty?
|
190
197
|
if db_name
|
191
|
-
|
192
|
-
|
198
|
+
if databases.first.permission == :import_only
|
199
|
+
$stderr.puts "Database '#{db_name}' is import only, cannot list or create tables."
|
200
|
+
else
|
201
|
+
$stderr.puts "Database '#{db_name}' has no tables."
|
202
|
+
$stderr.puts "Use '#{$prog} " + Config.cl_options_string + "table:create <db> <table>' to create a table."
|
203
|
+
end
|
193
204
|
elsif databases.empty?
|
194
205
|
$stderr.puts "There are no databases."
|
195
206
|
$stderr.puts "Use '#{$prog} " + Config.cl_options_string + "db:create <db>' to create a database."
|
data/lib/td/command/user.rb
CHANGED
@@ -120,7 +120,7 @@ module Command
|
|
120
120
|
client.add_user(name, nil, email, password)
|
121
121
|
|
122
122
|
$stderr.puts "User '#{name}' is created."
|
123
|
-
$stderr.puts "Use '#{$prog} " + Config.cl_options_string + "user:apikeys #{name}' to show the API key."
|
123
|
+
$stderr.puts "Use '#{$prog} " + Config.cl_options_string + "user:apikeys #{name}' to show the API key."
|
124
124
|
end
|
125
125
|
|
126
126
|
def user_delete(op)
|
data/lib/td/updater.rb
CHANGED
@@ -330,35 +330,35 @@ end # module ModuleDefinition
|
|
330
330
|
|
331
331
|
if updated > last_updated
|
332
332
|
FileUtils.mkdir_p(Updater.jarfile_dest_path) unless File.exists?(Updater.jarfile_dest_path)
|
333
|
-
Dir.chdir
|
333
|
+
Dir.chdir(Updater.jarfile_dest_path) do
|
334
|
+
File.open('VERSION', 'w') {|f|
|
335
|
+
if hourly
|
336
|
+
f.print "#{version} via hourly jar auto-update"
|
337
|
+
else
|
338
|
+
f.print "#{version} via import:jar_update command"
|
339
|
+
end
|
340
|
+
}
|
341
|
+
File.open('td-import-java.version', 'w') {|f|
|
342
|
+
f.print "#{version} #{updated}"
|
343
|
+
}
|
334
344
|
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
File.open('td-import-java.version', 'w') {|f|
|
343
|
-
f.print "#{version} #{updated}"
|
344
|
-
}
|
345
|
-
|
346
|
-
status = nil
|
347
|
-
indicator = Command::TimeBasedDownloadProgressIndicator.new(
|
348
|
-
"Updating td-import.jar", Time.new.to_i, 2)
|
349
|
-
File.open('td-import.jar.new', 'wb') {|binfile|
|
350
|
-
status = Updater.stream_fetch("#{maven_repo}/#{version}/td-import-#{version}-jar-with-dependencies.jar", binfile) {
|
351
|
-
indicator.update
|
345
|
+
status = nil
|
346
|
+
indicator = Command::TimeBasedDownloadProgressIndicator.new(
|
347
|
+
"Updating td-import.jar", Time.new.to_i, 2)
|
348
|
+
File.open('td-import.jar.new', 'wb') {|binfile|
|
349
|
+
status = Updater.stream_fetch("#{maven_repo}/#{version}/td-import-#{version}-jar-with-dependencies.jar", binfile) {
|
350
|
+
indicator.update
|
351
|
+
}
|
352
352
|
}
|
353
|
-
|
354
|
-
indicator.finish()
|
353
|
+
indicator.finish()
|
355
354
|
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
355
|
+
if status
|
356
|
+
puts "Installed td-import.jar v#{version} in '#{Updater.jarfile_dest_path}'.\n"
|
357
|
+
File.rename 'td-import.jar.new', 'td-import.jar'
|
358
|
+
else
|
359
|
+
puts "Update of td-import.jar failed." unless ENV['TD_TOOLBELT_DEBUG'].nil?
|
360
|
+
File.delete 'td-import.jar.new' if File.exists? 'td-import.jar.new'
|
361
|
+
end
|
362
362
|
end
|
363
363
|
else
|
364
364
|
puts 'Installed td-import.jar is already at the latest version.' unless hourly
|
data/lib/td/version.rb
CHANGED
data/spec/td/command/job_spec.rb
CHANGED
@@ -13,6 +13,8 @@ module TreasureData::Command
|
|
13
13
|
end
|
14
14
|
|
15
15
|
describe 'write_result' do
|
16
|
+
let(:file) { Tempfile.new("job_spec") }
|
17
|
+
|
16
18
|
let :job do
|
17
19
|
job = TreasureData::Job.new(nil, 12345, 'hive', 'select * from employee')
|
18
20
|
job.instance_eval do
|
@@ -23,22 +25,216 @@ module TreasureData::Command
|
|
23
25
|
job
|
24
26
|
end
|
25
27
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
28
|
+
context 'result without nil' do
|
29
|
+
it 'supports json output' do
|
30
|
+
command.send(:show_result, job, file, nil, 'json')
|
31
|
+
File.read(file.path).should == %Q([["1",2.0,{"key":3}],\n["4",5.0,{"key":6}],\n["7",8.0,{"key":9}]])
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'supports csv output' do
|
35
|
+
command.send(:show_result, job, file, nil, 'csv')
|
36
|
+
File.read(file.path).should == %Q(1,2.0,"{""key"":3}"\n4,5.0,"{""key"":6}"\n7,8.0,"{""key"":9}"\n)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'supports tsv output' do
|
40
|
+
command.send(:show_result, job, file, nil, 'tsv')
|
41
|
+
File.read(file.path).should == %Q(1\t2.0\t{"key":3}\n4\t5.0\t{"key":6}\n7\t8.0\t{"key":9}\n)
|
42
|
+
end
|
30
43
|
end
|
31
44
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
45
|
+
context 'result with nil' do
|
46
|
+
let :job_id do
|
47
|
+
12345
|
48
|
+
end
|
49
|
+
|
50
|
+
let :job do
|
51
|
+
job = TreasureData::Job.new(nil, job_id, 'hive', 'select * from employee')
|
52
|
+
job.instance_eval do
|
53
|
+
@result = [[[nil, 2.0, {key:3}], 1]]
|
54
|
+
@result_size = 3
|
55
|
+
@status = 'success'
|
56
|
+
end
|
57
|
+
job
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'with --column-header option' do
|
61
|
+
before do
|
62
|
+
job.stub(:hive_result_schema).and_return([['c0', 'time'], ['c1', 'double'], ['v', nil], ['c3', 'long']])
|
63
|
+
client = Object.new
|
64
|
+
client.stub(:job).with(job_id).and_return(job)
|
65
|
+
command.stub(:get_client).and_return(client)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'supports json output' do
|
69
|
+
command.send(:show_result, job, file, nil, 'json', { header: true })
|
70
|
+
File.read(file.path).should == %Q([[null,2.0,{"key":3}]])
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'supports csv output' do
|
74
|
+
command.send(:show_result, job, file, nil, 'csv', { header: true })
|
75
|
+
File.read(file.path).should == %Q(c0,c1,v,c3\nnull,2.0,"{""key"":3}"\n)
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'supports tsv output' do
|
79
|
+
command.send(:show_result, job, file, nil, 'tsv', { header: true })
|
80
|
+
File.read(file.path).should == %Q(c0\tc1\tv\tc3\nnull\t2.0\t{"key":3}\n)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'without --null option' do
|
85
|
+
it 'supports json output' do
|
86
|
+
command.send(:show_result, job, file, nil, 'json')
|
87
|
+
File.read(file.path).should == %Q([[null,2.0,{"key":3}]])
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'supports csv output' do
|
91
|
+
command.send(:show_result, job, file, nil, 'csv')
|
92
|
+
File.read(file.path).should == %Q(null,2.0,"{""key"":3}"\n)
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'supports tsv output' do
|
96
|
+
command.send(:show_result, job, file, nil, 'tsv')
|
97
|
+
File.read(file.path).should == %Q(null\t2.0\t{"key":3}\n)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context 'with --null option' do
|
102
|
+
it 'dose not effect json output (nil will be shown as null)' do
|
103
|
+
command.send(:show_result, job, file, nil, 'json', { null_expr: "NULL" })
|
104
|
+
File.read(file.path).should == %Q([[null,2.0,{"key":3}]])
|
105
|
+
end
|
106
|
+
|
107
|
+
context 'csv format' do
|
108
|
+
context 'specified string is NULL' do
|
109
|
+
let!(:null_expr) { "NULL" }
|
110
|
+
|
111
|
+
it 'shows nill as specified string' do
|
112
|
+
command.send(:show_result, job, file, nil, 'csv', { null_expr: null_expr })
|
113
|
+
File.read(file.path).should == %Q(NULL,2.0,"{""key"":3}"\n)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context 'specified string is empty string' do
|
118
|
+
let!(:null_expr) { '' }
|
119
|
+
|
120
|
+
it 'shows nill as empty string' do
|
121
|
+
command.send(:show_result, job, file, nil, 'csv', { null_expr: null_expr })
|
122
|
+
File.read(file.path).should == %Q("",2.0,"{""key"":3}"\n)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'supports tsv output' do
|
128
|
+
command.send(:show_result, job, file, nil, 'tsv', { null_expr: "\"\"" })
|
129
|
+
File.read(file.path).should == %Q(""\t2.0\t{"key":3}\n)
|
130
|
+
end
|
131
|
+
end
|
36
132
|
end
|
37
133
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
134
|
+
context 'without NaN/Infinity' do
|
135
|
+
|
136
|
+
it 'supports json output' do
|
137
|
+
command.send(:show_result, job, file, nil, 'json')
|
138
|
+
File.read(file.path).should == %Q([["1",2.0,{"key":3}],\n["4",5.0,{"key":6}],\n["7",8.0,{"key":9}]])
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'supports csv output' do
|
142
|
+
command.send(:show_result, job, file, nil, 'csv')
|
143
|
+
File.read(file.path).should == %Q(1,2.0,"{""key"":3}"\n4,5.0,"{""key"":6}"\n7,8.0,"{""key"":9}"\n)
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'supports tsv output' do
|
147
|
+
command.send(:show_result, job, file, nil, 'tsv')
|
148
|
+
File.read(file.path).should == %Q(1\t2.0\t{"key":3}\n4\t5.0\t{"key":6}\n7\t8.0\t{"key":9}\n)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
context 'with NaN/Infinity' do
|
153
|
+
let :job do
|
154
|
+
job = TreasureData::Job.new(nil, 12345, 'hive', 'select * from employee')
|
155
|
+
job.instance_eval do
|
156
|
+
@result = [[[0.0/0.0, 1.0/0.0, 1.0/-0.0], 1], [["4", 5.0, {key:6}], 2], [["7", 8.0, {key:9}], 3]]
|
157
|
+
@result_size = 3
|
158
|
+
@status = 'success'
|
159
|
+
end
|
160
|
+
job
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'does not support json output' do
|
164
|
+
expect { command.send(:show_result, job, file, nil, 'json') }.to raise_error Yajl::EncodeError
|
165
|
+
end
|
166
|
+
|
167
|
+
it 'supports csv output' do
|
168
|
+
command.send(:show_result, job, file, nil, 'csv')
|
169
|
+
File.read(file.path).should == %Q("""NaN""","""Infinity""","""-Infinity"""\n4,5.0,"{""key"":6}"\n7,8.0,"{""key"":9}"\n)
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'supports tsv output' do
|
173
|
+
command.send(:show_result, job, file, nil, 'tsv')
|
174
|
+
File.read(file.path).should == %Q("NaN"\t"Infinity"\t"-Infinity"\n4\t5.0\t{"key":6}\n7\t8.0\t{"key":9}\n)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
describe '#job_show' do
|
180
|
+
let(:job_id) { "12345" }
|
181
|
+
|
182
|
+
let :job_classs do
|
183
|
+
Struct.new(:job_id,
|
184
|
+
:status,
|
185
|
+
:type,
|
186
|
+
:db_name,
|
187
|
+
:priority,
|
188
|
+
:retry_limit,
|
189
|
+
:result_url,
|
190
|
+
:query,
|
191
|
+
:cpu_time,
|
192
|
+
:result_size
|
193
|
+
)
|
194
|
+
end
|
195
|
+
|
196
|
+
let :job do
|
197
|
+
job_classs.new(job_id,
|
198
|
+
nil,
|
199
|
+
:hive,
|
200
|
+
"db_name",
|
201
|
+
1,
|
202
|
+
1,
|
203
|
+
"test_url",
|
204
|
+
"test_qury",
|
205
|
+
1,
|
206
|
+
3
|
207
|
+
)
|
208
|
+
end
|
209
|
+
|
210
|
+
before do
|
211
|
+
job.stub(:finished?).and_return(true)
|
212
|
+
|
213
|
+
client = Object.new
|
214
|
+
client.stub(:job).with(job_id).and_return(job)
|
215
|
+
command.stub(:get_client).and_return(client)
|
216
|
+
end
|
217
|
+
|
218
|
+
context 'without --null option' do
|
219
|
+
it 'calls #show_result without null_expr option' do
|
220
|
+
command.stub(:show_result).with(job, nil, nil, nil, {:header=>false})
|
221
|
+
op = List::CommandParser.new("job:show", %w[job_id], %w[], nil, ["12345"], true)
|
222
|
+
command.job_show(op)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
context 'with --null option' do
|
227
|
+
it 'calls #show_result with null_expr option' do
|
228
|
+
command.stub(:show_result).with(job, nil, nil, nil, {:header=>false, :null_expr=>"NULL"} )
|
229
|
+
op = List::CommandParser.new("job:show", %w[job_id], %w[], nil, ["12345", "--null", "NULL"], true)
|
230
|
+
command.job_show(op)
|
231
|
+
end
|
232
|
+
|
233
|
+
it 'calls #show_result with null_expr option' do
|
234
|
+
command.stub(:show_result).with(job, nil, nil, nil, {:header=>false, :null_expr=>'""'} )
|
235
|
+
op = List::CommandParser.new("job:show", %w[job_id], %w[], nil, ["12345", "--null", '""'], true)
|
236
|
+
command.job_show(op)
|
237
|
+
end
|
42
238
|
end
|
43
239
|
end
|
44
240
|
|
File without changes
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'td/command/common'
|
3
|
+
require 'td/config'
|
4
|
+
require 'td/command/list'
|
5
|
+
require 'td/command/table'
|
6
|
+
require 'td/client/model'
|
7
|
+
|
8
|
+
module TreasureData::Command
|
9
|
+
|
10
|
+
describe 'table command' do
|
11
|
+
describe 'table_list' do
|
12
|
+
it 'lists tables in a database' do
|
13
|
+
client = Object.new
|
14
|
+
|
15
|
+
db = TreasureData::Database.new(client, 'full_access_db', nil, 1000, Time.now.to_i, Time.now.to_i, nil, 'full_access')
|
16
|
+
|
17
|
+
create_tables = lambda {|db_name|
|
18
|
+
(1..6).map {|i|
|
19
|
+
schema = TreasureData::Schema.new.from_json(JSON.parse('[]'))
|
20
|
+
TreasureData::Table.new(client, db_name, db_name + "_table_#{i}", 'log', schema, 500, Time.now.to_i, Time.now.to_i, 0, nil, nil, nil, nil, nil)
|
21
|
+
}
|
22
|
+
}
|
23
|
+
db_tables = create_tables.call(db.name)
|
24
|
+
|
25
|
+
client.stub(:tables).with(db.name).and_return(db_tables)
|
26
|
+
|
27
|
+
command = Class.new { include TreasureData::Command }.new
|
28
|
+
command.stub(:get_client).and_return(client)
|
29
|
+
command.stub(:get_database).and_return(db)
|
30
|
+
|
31
|
+
op = List::CommandParser.new('table:list', %w[], %w[db], false, %w(full_access_db), true)
|
32
|
+
expect {
|
33
|
+
command.table_list(op)
|
34
|
+
}.to_not raise_exception
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'lists all tables in all databases' do
|
38
|
+
client = Object.new
|
39
|
+
|
40
|
+
qo_db = TreasureData::Database.new(client, 'query_only_db', nil, 2000, Time.now.to_i, Time.now.to_i, nil, 'query_only')
|
41
|
+
fa_db = TreasureData::Database.new(client, 'full_access_db', nil, 3000, Time.now.to_i, Time.now.to_i, nil, 'full_access')
|
42
|
+
own_db = TreasureData::Database.new(client, 'owner_db', nil, 4000, Time.now.to_i, Time.now.to_i, nil, 'owner')
|
43
|
+
|
44
|
+
create_tables = lambda {|db_name|
|
45
|
+
(1..6).map {|i|
|
46
|
+
schema = TreasureData::Schema.new.from_json(JSON.parse('[]'))
|
47
|
+
TreasureData::Table.new(client, db_name, db_name + "_table_#{i}", 'log', schema, 500, Time.now.to_i, Time.now.to_i, 0, nil, nil, nil, nil, nil)
|
48
|
+
}
|
49
|
+
}
|
50
|
+
qo_db_tables = create_tables.call(qo_db.name)
|
51
|
+
fa_db_tables = create_tables.call(fa_db.name)
|
52
|
+
own_db_tables = create_tables.call(own_db.name)
|
53
|
+
|
54
|
+
client.stub(:databases).and_return([qo_db, fa_db, own_db])
|
55
|
+
|
56
|
+
client.stub(:tables).with(qo_db.name).and_return(qo_db_tables)
|
57
|
+
client.stub(:tables).with(fa_db.name).and_return(fa_db_tables)
|
58
|
+
client.stub(:tables).with(own_db.name).and_return(own_db_tables)
|
59
|
+
|
60
|
+
command = Class.new { include TreasureData::Command }.new
|
61
|
+
command.stub(:get_client).and_return(client)
|
62
|
+
|
63
|
+
op = List::CommandParser.new('table:list', %w[], %w[db], false, %w(), true)
|
64
|
+
expect {
|
65
|
+
command.table_list(op)
|
66
|
+
}.to_not raise_exception
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'avoids listing tables of an \'import_only\' database' do
|
70
|
+
client = Object.new
|
71
|
+
|
72
|
+
db = TreasureData::Database.new(client, 'import_only_db', nil, 1234, Time.now.to_i, Time.now.to_i, nil, 'import_only')
|
73
|
+
|
74
|
+
command = Class.new { include TreasureData::Command }.new
|
75
|
+
command.stub(:get_client).and_return(client)
|
76
|
+
command.stub(:get_database).and_return(db)
|
77
|
+
|
78
|
+
op = List::CommandParser.new('table:list', %w[], %w[db], false, %w(import_only_db), true)
|
79
|
+
expect {
|
80
|
+
command.table_list(op)
|
81
|
+
}.to_not raise_exception
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'avoids listing tables of the \'import_only\' databases in the list' do
|
85
|
+
client = Object.new
|
86
|
+
|
87
|
+
io_db = TreasureData::Database.new(client, 'import_only_db', nil, 1000, Time.now.to_i, Time.now.to_i, nil, 'import_only')
|
88
|
+
qo_db = TreasureData::Database.new(client, 'query_only_db', nil, 2000, Time.now.to_i, Time.now.to_i, nil, 'query_only')
|
89
|
+
fa_db = TreasureData::Database.new(client, 'full_access_db', nil, 3000, Time.now.to_i, Time.now.to_i, nil, 'full_access')
|
90
|
+
own_db = TreasureData::Database.new(client, 'owner_db', nil, 4000, Time.now.to_i, Time.now.to_i, nil, 'owner')
|
91
|
+
|
92
|
+
create_tables = lambda {|db_name|
|
93
|
+
(1..6).map {|i|
|
94
|
+
schema = TreasureData::Schema.new.from_json(JSON.parse('[]'))
|
95
|
+
TreasureData::Table.new(client, db_name, db_name + "_table_#{i}", 'log', schema, 500, Time.now.to_i, Time.now.to_i, 0, nil, nil, nil, nil, nil)
|
96
|
+
}
|
97
|
+
}
|
98
|
+
qo_db_tables = create_tables.call(qo_db.name)
|
99
|
+
fa_db_tables = create_tables.call(fa_db.name)
|
100
|
+
own_db_tables = create_tables.call(own_db.name)
|
101
|
+
|
102
|
+
client.stub(:databases).and_return([io_db, qo_db, fa_db, own_db])
|
103
|
+
|
104
|
+
client.stub(:tables).with(io_db.name).and_raise("not permitted")
|
105
|
+
client.stub(:tables).with(qo_db.name).and_return(qo_db_tables)
|
106
|
+
client.stub(:tables).with(fa_db.name).and_return(fa_db_tables)
|
107
|
+
client.stub(:tables).with(own_db.name).and_return(own_db_tables)
|
108
|
+
|
109
|
+
command = Class.new { include TreasureData::Command }.new
|
110
|
+
command.stub(:get_client).and_return(client)
|
111
|
+
|
112
|
+
op = List::CommandParser.new('table:list', %w[], %w[db], false, %w(), true)
|
113
|
+
expect {
|
114
|
+
command.table_list(op)
|
115
|
+
}.to_not raise_exception
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "number format" do
|
119
|
+
let(:number_raw) { "1234567" }
|
120
|
+
let(:number_format) { "1,234,567" }
|
121
|
+
let(:client) { double('null object').as_null_object }
|
122
|
+
let(:db) { TreasureData::Database.new(client, 'full_access_db', nil, 1000, Time.now.to_i, Time.now.to_i, nil, 'full_access') }
|
123
|
+
let(:command) do
|
124
|
+
command = Class.new { include TreasureData::Command }.new
|
125
|
+
command.stub(:get_client).and_return(client)
|
126
|
+
command.stub(:get_database).and_return(db)
|
127
|
+
command
|
128
|
+
end
|
129
|
+
|
130
|
+
before do
|
131
|
+
create_tables = lambda {|db_name|
|
132
|
+
(1..6).map {|i|
|
133
|
+
# NOTE: TreasureData::Helpers.format_with_delimiter uses `gsub!` to their argument
|
134
|
+
# the argument (in our case, `number_raw`) will be rewritten by them
|
135
|
+
# To avoid that behavior, pass `number_raw.dup` instead of `number_raw`
|
136
|
+
schema = TreasureData::Schema.new.from_json(JSON.parse('[]'))
|
137
|
+
TreasureData::Table.new(client, db_name, db_name + "_table_#{i}", 'log', schema, number_raw.dup, Time.now.to_i, Time.now.to_i, 0, nil, nil, nil, nil, nil)
|
138
|
+
}
|
139
|
+
}
|
140
|
+
db_tables = create_tables.call(db.name)
|
141
|
+
client.stub(:tables).with(db.name).and_return(db_tables)
|
142
|
+
end
|
143
|
+
|
144
|
+
subject do
|
145
|
+
# command.table_list uses `puts` to display result
|
146
|
+
# so temporary swapping $stdout with StringIO to fetch their output
|
147
|
+
backup = $stdout.dup
|
148
|
+
buf = StringIO.new
|
149
|
+
op = List::CommandParser.new('table:list', [], %w[db], false, options + %w(full_access_db), true)
|
150
|
+
begin
|
151
|
+
$stdout = buf
|
152
|
+
command.table_list(op)
|
153
|
+
$stdout.rewind
|
154
|
+
$stdout.read
|
155
|
+
ensure
|
156
|
+
$stdout = backup
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
context "without --format" do
|
161
|
+
let(:options) { [] }
|
162
|
+
it { should include(number_format) }
|
163
|
+
it { should_not include(number_raw) }
|
164
|
+
end
|
165
|
+
|
166
|
+
context "with --format table" do
|
167
|
+
let(:options) { %w(--format table) }
|
168
|
+
it { should include(number_format) }
|
169
|
+
it { should_not include(number_raw) }
|
170
|
+
end
|
171
|
+
|
172
|
+
context "with --format csv" do
|
173
|
+
let(:options) { %w(--format csv) }
|
174
|
+
it { should_not include(number_format) }
|
175
|
+
it { should include(number_raw) }
|
176
|
+
end
|
177
|
+
|
178
|
+
context "with --format tsv" do
|
179
|
+
let(:options) { %w(--format tsv) }
|
180
|
+
it { should_not include(number_format) }
|
181
|
+
it { should include(number_raw) }
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
data/spec/td/updater_spec.rb
CHANGED
@@ -105,6 +105,42 @@ module TreasureData::Updater
|
|
105
105
|
end
|
106
106
|
end
|
107
107
|
|
108
|
+
describe "current working directory doesn't change after call `jar_update`" do
|
109
|
+
shared_examples_for("jar_update behavior") do
|
110
|
+
it "doesn't change cwd" do
|
111
|
+
with_env('TD_TOOLBELT_JARUPDATE_ROOT', "https://localhost:#{@server.config[:Port]}") do
|
112
|
+
pwd = Dir.pwd
|
113
|
+
subject
|
114
|
+
expect(Dir.pwd).to eq pwd
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
it "don't exists td-import.jar.new" do
|
119
|
+
with_env('TD_TOOLBELT_JARUPDATE_ROOT', "https://localhost:#{@server.config[:Port]}") do
|
120
|
+
subject
|
121
|
+
end
|
122
|
+
tmpfile = File.join(TreasureData::Updater.jarfile_dest_path, 'td-import.jar.new')
|
123
|
+
expect(File.exists?(tmpfile)).to eq false
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
let(:updater) { JarUpdateTester.new }
|
128
|
+
|
129
|
+
subject { updater.kick }
|
130
|
+
|
131
|
+
context "not updated" do
|
132
|
+
before { updater.stub(:existent_jar_updated_time).and_return(Time.now) }
|
133
|
+
|
134
|
+
it_behaves_like "jar_update behavior"
|
135
|
+
end
|
136
|
+
|
137
|
+
context "updated" do
|
138
|
+
before { updater.stub(:existent_jar_updated_time).and_return(Time.at(0)) }
|
139
|
+
|
140
|
+
it_behaves_like "jar_update behavior"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
108
144
|
def with_proxy
|
109
145
|
with_env('HTTP_PROXY', "http://localhost:#{@proxy_server.config[:Port]}") do
|
110
146
|
yield
|
@@ -185,7 +221,7 @@ module TreasureData::Updater
|
|
185
221
|
res.body = '<metadata><versioning><lastUpdated>20141204123456</lastUpdated><release>version</release></versioning></metadata>'
|
186
222
|
end
|
187
223
|
|
188
|
-
def jar
|
224
|
+
def jar(req, res)
|
189
225
|
res['content-type'] = 'application/octet-stream'
|
190
226
|
res.body = File.read(fixture_file('tmp.zip'))
|
191
227
|
end
|
data/td.gemspec
CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |gem|
|
|
21
21
|
gem.add_dependency "yajl-ruby", "~> 1.1"
|
22
22
|
gem.add_dependency "hirb", ">= 0.4.5"
|
23
23
|
gem.add_dependency "parallel", "~> 0.6.1"
|
24
|
-
gem.add_dependency "td-client", "~> 0.8.
|
24
|
+
gem.add_dependency "td-client", "~> 0.8.69"
|
25
25
|
gem.add_dependency "td-logger", "~> 0.3.21"
|
26
26
|
gem.add_dependency "rubyzip", "~> 0.9.9"
|
27
27
|
gem.add_development_dependency "rake", "~> 0.9"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: td
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.11.
|
4
|
+
version: 0.11.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Treasure Data, Inc.
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-04-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: msgpack
|
@@ -102,14 +102,14 @@ dependencies:
|
|
102
102
|
requirements:
|
103
103
|
- - "~>"
|
104
104
|
- !ruby/object:Gem::Version
|
105
|
-
version: 0.8.
|
105
|
+
version: 0.8.69
|
106
106
|
type: :runtime
|
107
107
|
prerelease: false
|
108
108
|
version_requirements: !ruby/object:Gem::Requirement
|
109
109
|
requirements:
|
110
110
|
- - "~>"
|
111
111
|
- !ruby/object:Gem::Version
|
112
|
-
version: 0.8.
|
112
|
+
version: 0.8.69
|
113
113
|
- !ruby/object:Gem::Dependency
|
114
114
|
name: td-logger
|
115
115
|
requirement: !ruby/object:Gem::Requirement
|
@@ -204,6 +204,7 @@ files:
|
|
204
204
|
- lib/td/command/apikey.rb
|
205
205
|
- lib/td/command/bulk_import.rb
|
206
206
|
- lib/td/command/common.rb
|
207
|
+
- lib/td/command/connector.rb
|
207
208
|
- lib/td/command/db.rb
|
208
209
|
- lib/td/command/export.rb
|
209
210
|
- lib/td/command/help.rb
|
@@ -239,7 +240,8 @@ files:
|
|
239
240
|
- spec/spec_helper.rb
|
240
241
|
- spec/td/command/import_spec.rb
|
241
242
|
- spec/td/command/job_spec.rb
|
242
|
-
- spec/td/
|
243
|
+
- spec/td/command/sched_spec.rb
|
244
|
+
- spec/td/command/table_spec.rb
|
243
245
|
- spec/td/common_spec.rb
|
244
246
|
- spec/td/fixture/ca.cert
|
245
247
|
- spec/td/fixture/server.cert
|
@@ -282,7 +284,8 @@ test_files:
|
|
282
284
|
- spec/spec_helper.rb
|
283
285
|
- spec/td/command/import_spec.rb
|
284
286
|
- spec/td/command/job_spec.rb
|
285
|
-
- spec/td/
|
287
|
+
- spec/td/command/sched_spec.rb
|
288
|
+
- spec/td/command/table_spec.rb
|
286
289
|
- spec/td/common_spec.rb
|
287
290
|
- spec/td/fixture/ca.cert
|
288
291
|
- spec/td/fixture/server.cert
|