rsql 0.1.5 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +22 -7
- data/bin/rsql +62 -15
- data/example.rsqlrc +4 -4
- data/lib/rsql.rb +1 -1
- data/lib/rsql/commands.rb +5 -3
- data/lib/rsql/eval_context.rb +20 -1
- data/lib/rsql/mysql_results.rb +40 -22
- data/test/dummy_mysql.rb +11 -0
- data/test/test_commands.rb +84 -0
- data/test/test_eval_context.rb +127 -0
- data/test/test_mysql_results.rb +122 -0
- metadata +10 -5
- data/TODO +0 -37
data/README.rdoc
CHANGED
@@ -13,6 +13,11 @@ to an intermediary host for access to the SQL server.
|
|
13
13
|
|
14
14
|
gem install rsql
|
15
15
|
|
16
|
+
Alternatively, RSQL can be downloaded as a tar.gz or zip and run
|
17
|
+
directly from within the unpacked source directory. The only
|
18
|
+
requirement is that SSH functionality will be disabled unless the
|
19
|
+
Net::SSH Ruby library is installed and available.
|
20
|
+
|
16
21
|
== USAGE
|
17
22
|
|
18
23
|
RSQL is invoked from the comamnd line using:
|
@@ -45,22 +50,32 @@ RSQL is invoked from the comamnd line using:
|
|
45
50
|
had been provided at the RSQL prompt interactively. This option
|
46
51
|
*must* be the last option specified.
|
47
52
|
|
48
|
-
The _ssh_host_ and _mysql_host_
|
49
|
-
_password_ values using the following syntax:
|
53
|
+
The _ssh_host_ and _mysql_host_ arguments may optionally include
|
54
|
+
_user_ and _password_ values using the following syntax:
|
50
55
|
|
51
56
|
[<user>[:<password>]@]<host>
|
52
57
|
|
53
|
-
|
54
|
-
|
58
|
+
An empty password can be provided by simply listing nothing between
|
59
|
+
demarcation points:
|
55
60
|
|
56
61
|
root:@127.0.0.1
|
57
62
|
|
58
63
|
Once at the +rsql+ prompt, normal MySQL queries can be entered as
|
59
|
-
expected, ending each with a semicolon (;)
|
64
|
+
expected, ending each with a semicolon (;) for columnar output or \G
|
65
|
+
for line-by-line output formatting.
|
66
|
+
|
67
|
+
Ruby commands will be evaluated for any content entered at the RSQL
|
68
|
+
prompt beginning with a period (.).
|
69
|
+
|
70
|
+
== GETTING STARTED
|
71
|
+
|
72
|
+
This example walks through many features of RSQL including how to
|
73
|
+
build up recipes:
|
60
74
|
|
61
|
-
|
75
|
+
https://github.com/bradrf/rsql/raw/master/example.rsqlrc
|
62
76
|
|
63
|
-
|
77
|
+
The file is available as example.rsqlrc installed with the gem or
|
78
|
+
downloaded with the source.
|
64
79
|
|
65
80
|
== LICENSE
|
66
81
|
|
data/bin/rsql
CHANGED
@@ -31,7 +31,9 @@ require 'thread'
|
|
31
31
|
require 'timeout'
|
32
32
|
require 'readline'
|
33
33
|
require 'yaml'
|
34
|
-
|
34
|
+
|
35
|
+
# This is included below to make it optional if SSH is never needed.
|
36
|
+
# require 'net/ssh'
|
35
37
|
|
36
38
|
# allow ourselves to be run from within a source tree
|
37
39
|
if File.symlink?(__FILE__)
|
@@ -114,6 +116,7 @@ if i = ARGV.index('-batch')
|
|
114
116
|
end
|
115
117
|
|
116
118
|
if i = ARGV.index('-ssh')
|
119
|
+
require 'net/ssh'
|
117
120
|
ARGV.delete_at(i)
|
118
121
|
(ssh_host, ssh_user, ssh_password) = split_login(ARGV.delete_at(i))
|
119
122
|
end
|
@@ -152,7 +155,7 @@ USAGE
|
|
152
155
|
end
|
153
156
|
|
154
157
|
(mysql_host, mysql_user, mysql_password) = split_login(ARGV.shift)
|
155
|
-
mysql_password
|
158
|
+
mysql_password ||= get_password("#{mysql_host}@#{mysql_host} MySQL password: ")
|
156
159
|
real_mysql_host = mysql_host
|
157
160
|
|
158
161
|
if ssh_host
|
@@ -162,7 +165,7 @@ else
|
|
162
165
|
mysql_port = 3306
|
163
166
|
end
|
164
167
|
|
165
|
-
|
168
|
+
MySQLResults.database_name = ARGV.shift
|
166
169
|
|
167
170
|
unless $stdin.tty?
|
168
171
|
batch_input = $stdin.read.gsub(/\r?\n/,';')
|
@@ -192,7 +195,7 @@ rescue Timeout::Error
|
|
192
195
|
$stderr.puts "Timed out waiting to close #{name} connection"
|
193
196
|
false
|
194
197
|
rescue Exception => ex
|
195
|
-
$stderr.puts(ex)
|
198
|
+
$stderr.puts("#{ex.class}: #{ex.message}")
|
196
199
|
false
|
197
200
|
end
|
198
201
|
|
@@ -206,32 +209,65 @@ if ssh_host
|
|
206
209
|
# us...or just loop reconnection here in the thread...
|
207
210
|
|
208
211
|
port_opened = false
|
212
|
+
getting_password = false
|
213
|
+
password_retry_cnt = 0
|
209
214
|
|
210
215
|
puts "SSH #{ssh_user}#{ssh_user ? '@' : ''}#{ssh_host}..." unless batch_input
|
211
216
|
ssh = nil
|
212
217
|
ssh_thread = Thread.new do
|
213
218
|
opts = {:timeout => 15}
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
219
|
+
begin
|
220
|
+
opts[:password] = ssh_password if ssh_password
|
221
|
+
ssh = Net::SSH.start(ssh_host, ssh_user, opts)
|
222
|
+
ssh_enabled = true
|
223
|
+
rescue Net::SSH::AuthenticationFailed
|
224
|
+
if 2 < password_retry_cnt
|
225
|
+
$stderr.puts 'Permission denied. Giving up.'
|
226
|
+
else
|
227
|
+
$stderr.puts 'Permission denied, please try again.' if ssh_password
|
228
|
+
getting_password = true
|
229
|
+
ssh_password = get_password("#{ssh_user}@#{ssh_host} SSH password: ")
|
230
|
+
getting_password = false
|
231
|
+
unless ssh_password.empty?
|
232
|
+
password_retry_cnt += 1
|
233
|
+
retry
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
if ssh_enabled
|
238
|
+
ssh.forward.local(mysql_port, mysql_host, 3306)
|
239
|
+
port_opened = true
|
240
|
+
ssh.loop(1) { ssh_enabled }
|
241
|
+
end
|
220
242
|
end
|
221
243
|
|
222
244
|
15.times do
|
223
|
-
break if ssh_enabled && port_opened
|
245
|
+
break if !ssh_thread.alive? || (ssh_enabled && port_opened)
|
246
|
+
while getting_password
|
247
|
+
# give them extra time
|
248
|
+
sleep(1)
|
249
|
+
end
|
224
250
|
sleep(1)
|
225
251
|
end
|
226
252
|
|
227
253
|
unless ssh_enabled
|
228
|
-
$stderr.puts "failed to connect to #{ssh_host}
|
254
|
+
$stderr.puts "failed to connect to #{ssh_host} SSH host"
|
255
|
+
begin
|
256
|
+
ssh_thread.join
|
257
|
+
rescue Exception => ex
|
258
|
+
$stderr.puts(" => #{ex.message} (#{ex.class})")
|
259
|
+
end
|
229
260
|
exit 1
|
230
261
|
end
|
231
262
|
|
232
263
|
unless port_opened
|
233
264
|
$stderr.puts("failed to forward #{mysql_port}:#{mysql_host}:3306 via #{ssh_host} " \
|
234
265
|
"ssh host in 15 seconds")
|
266
|
+
begin
|
267
|
+
ssh_thread.join
|
268
|
+
rescue Exception => ex
|
269
|
+
$stderr.puts(" => #{ex.message}: #{ex.class}")
|
270
|
+
end
|
235
271
|
exit 1
|
236
272
|
end
|
237
273
|
|
@@ -241,7 +277,8 @@ end
|
|
241
277
|
|
242
278
|
puts "MySQL #{mysql_user}@#{real_mysql_host}..." unless batch_input
|
243
279
|
begin
|
244
|
-
MySQLResults.conn = Mysql.new(mysql_host, mysql_user, mysql_password,
|
280
|
+
MySQLResults.conn = Mysql.new(mysql_host, mysql_user, mysql_password,
|
281
|
+
MySQLResults.database_name, mysql_port)
|
245
282
|
rescue Mysql::Error => ex
|
246
283
|
if ex.message.include?('Client does not support authentication')
|
247
284
|
$stderr.puts "failed to connect to #{mysql_host} mysql server: unknown credentials?"
|
@@ -249,6 +286,17 @@ rescue Mysql::Error => ex
|
|
249
286
|
$stderr.puts "failed to connect to #{mysql_host} mysql server: #{ex.message}"
|
250
287
|
end
|
251
288
|
exit 1
|
289
|
+
rescue NoMethodError
|
290
|
+
# this happens when mysql tries to read four bytes and assume it
|
291
|
+
# can index into them even when read returned nil...this happens
|
292
|
+
# because the connect succeeds due to the SSH forwarded port but
|
293
|
+
# then there isn't anybody connected on the remote side of the
|
294
|
+
# proxy
|
295
|
+
$stderr.puts "failed to connect to #{mysql_host} mysql server"
|
296
|
+
exit 1
|
297
|
+
rescue Exception => ex
|
298
|
+
$stderr.puts "failed to connect to #{mysql_host} mysql server: #{ex.message} (#{ex.class})"
|
299
|
+
exit 1
|
252
300
|
end
|
253
301
|
|
254
302
|
eval_context.call_init_registrations
|
@@ -270,8 +318,7 @@ cmd_thread = Thread.new do
|
|
270
318
|
me[:shutdown] = true # only run once
|
271
319
|
input = batch_input
|
272
320
|
else
|
273
|
-
|
274
|
-
puts '',"[#{mysql_user}@#{ssh_host||mysql_host}:#{db_name}]"
|
321
|
+
puts '',"[#{mysql_user}@#{ssh_host||mysql_host}:#{MySQLResults.database_name}]"
|
275
322
|
input = ''
|
276
323
|
prompt = bn + '> '
|
277
324
|
loop do
|
data/example.rsqlrc
CHANGED
@@ -206,7 +206,7 @@ end
|
|
206
206
|
# say if we want to process results and keep our data around in a
|
207
207
|
# file.
|
208
208
|
#
|
209
|
-
# rsql> select name, value from rsql_example | save_values 'myobj'
|
209
|
+
# rsql> select name, value from rsql_example | save_values 'myobj';
|
210
210
|
#
|
211
211
|
# After running this, a myobj.yml file should be created in the local
|
212
212
|
# directory containing all the content from the query. To accomplish
|
@@ -225,7 +225,7 @@ end
|
|
225
225
|
# Dealing with variable arguments is pretty straightforward as well,
|
226
226
|
# but with a little syntactic twist.
|
227
227
|
#
|
228
|
-
# rsql> .find_names 'fancy3', 'fancy8'
|
228
|
+
# rsql> .find_names 'fancy3', 'fancy8';
|
229
229
|
#
|
230
230
|
# Here we simply expand the arguments.
|
231
231
|
#
|
@@ -259,14 +259,14 @@ end
|
|
259
259
|
# to our consoles than just dumping it. It converts it into a
|
260
260
|
# hexadecimal string.
|
261
261
|
#
|
262
|
-
# rsql> SELECT stuff FROM rsql_example
|
262
|
+
# rsql> SELECT stuff FROM rsql_example;
|
263
263
|
#
|
264
264
|
# The default is to limit the hex strings to 32 "bytes" reported. This
|
265
265
|
# can be configured any time by setting the @hexstr_limit.
|
266
266
|
#
|
267
267
|
# RSQL makes querying for hex strings from within a recipe easy too.
|
268
268
|
#
|
269
|
-
# rsql> .find_stuff 0x346950d3c051c8ac51092a3a2eff7503ef7f571c
|
269
|
+
# rsql> .find_stuff 0x346950d3c051c8ac51092a3a2eff7503ef7f571c;
|
270
270
|
#
|
271
271
|
register :find_stuff, :stuff, %q{
|
272
272
|
SELECT * FROM #{@rsql_table} WHERE stuff=#{hexify stuff}
|
data/lib/rsql.rb
CHANGED
data/lib/rsql/commands.rb
CHANGED
@@ -49,6 +49,8 @@ module RSQL
|
|
49
49
|
next_is_ruby = false
|
50
50
|
|
51
51
|
input.scan(/[^#{SEPARATORS}]+.?/) do |match|
|
52
|
+
orig_match = match
|
53
|
+
|
52
54
|
if i = SEPARATORS.index(match[-1])
|
53
55
|
sep = SEPARATORS[i]
|
54
56
|
match.chop!
|
@@ -101,7 +103,7 @@ module RSQL
|
|
101
103
|
in_pipe_arg = false
|
102
104
|
esc << match << '|'
|
103
105
|
next
|
104
|
-
elsif
|
106
|
+
elsif orig_match =~ /\{\s*|do\s*/
|
105
107
|
in_pipe_arg = true
|
106
108
|
esc << match << '|'
|
107
109
|
next
|
@@ -190,7 +192,7 @@ module RSQL
|
|
190
192
|
stdout = cmd.displayer == :pipe ? StringIO.new : nil
|
191
193
|
value = eval_context.safe_eval(cmd.content, last_results, stdout)
|
192
194
|
if String === value
|
193
|
-
cmds = Commands.new(value,
|
195
|
+
cmds = Commands.new(value, cmd.displayer)
|
194
196
|
unless cmds.empty?
|
195
197
|
# need to carry along the bangs into the
|
196
198
|
# last command so we don't lose them
|
@@ -212,7 +214,7 @@ module RSQL
|
|
212
214
|
last_results = MySQLResults.query(value, eval_context)
|
213
215
|
rescue MySQLResults::MaxRowsException => ex
|
214
216
|
$stderr.puts "refusing to process #{ex.rows} rows (max: #{ex.max})"
|
215
|
-
rescue
|
217
|
+
rescue Mysql::Error => ex
|
216
218
|
$stderr.puts ex.message
|
217
219
|
rescue Exception => ex
|
218
220
|
$stderr.puts ex.inspect
|
data/lib/rsql/eval_context.rb
CHANGED
@@ -21,6 +21,8 @@
|
|
21
21
|
|
22
22
|
module RSQL
|
23
23
|
|
24
|
+
require 'time'
|
25
|
+
|
24
26
|
################################################################################
|
25
27
|
# This class wraps all dynamic evaluation and serves as the reflection
|
26
28
|
# class for adding methods dynamically.
|
@@ -283,6 +285,13 @@ module RSQL
|
|
283
285
|
@registrations[sym] = Registration.new(name, args, bangs, block, usage, desc)
|
284
286
|
end
|
285
287
|
|
288
|
+
# Convert a list of values into a comma-delimited string,
|
289
|
+
# optionally with each value in single quotes.
|
290
|
+
#
|
291
|
+
def to_list(vals, quoted=false) # :doc:
|
292
|
+
vals.collect{|v| quoted ? "'#{v}'" : v.to_s}.join(',')
|
293
|
+
end
|
294
|
+
|
286
295
|
# Convert a collection of values into hexadecimal strings.
|
287
296
|
#
|
288
297
|
def hexify(*ids) # :doc:
|
@@ -304,7 +313,7 @@ module RSQL
|
|
304
313
|
|
305
314
|
# Convert a number of bytes into a human readable string.
|
306
315
|
#
|
307
|
-
def humanize_bytes(bytes)
|
316
|
+
def humanize_bytes(bytes) # :doc:
|
308
317
|
abbrev = ['B','KB','MB','GB','TB','PB','EB','ZB','YB']
|
309
318
|
bytes = bytes.to_i
|
310
319
|
fmt = '%7.2f'
|
@@ -341,6 +350,16 @@ module RSQL
|
|
341
350
|
raise "unable to parse '#{str}'"
|
342
351
|
end
|
343
352
|
|
353
|
+
# Show a nice percent value of a decimal string.
|
354
|
+
#
|
355
|
+
def humanize_percentage(decimal, precision=1) # :doc:
|
356
|
+
if decimal.nil? || decimal == 'NULL'
|
357
|
+
'NA'
|
358
|
+
else
|
359
|
+
"%5.#{precision}f%%" % (decimal.to_f * 100)
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
344
363
|
# Convert a time into a relative string from now.
|
345
364
|
#
|
346
365
|
def relative_time(dt) # :doc:
|
data/lib/rsql/mysql_results.rb
CHANGED
@@ -32,6 +32,7 @@ module RSQL
|
|
32
32
|
@@field_separator = ' '
|
33
33
|
@@max_rows = 1000
|
34
34
|
@@database_name = nil
|
35
|
+
@@databases = nil
|
35
36
|
|
36
37
|
class MaxRowsException < RangeError
|
37
38
|
def initialize(rows, max)
|
@@ -56,35 +57,48 @@ module RSQL
|
|
56
57
|
#
|
57
58
|
def database_name; @@database_name; end
|
58
59
|
|
60
|
+
# Set the name of the current database in use.
|
61
|
+
#
|
62
|
+
def database_name=(database); @@database_name = database; end
|
63
|
+
|
59
64
|
# Get the list of databases available.
|
60
65
|
#
|
61
66
|
def databases
|
62
|
-
|
67
|
+
if @@conn && @@databases.nil?
|
68
|
+
@@databases = @@conn.list_dbs.sort
|
69
|
+
end
|
70
|
+
@@databases
|
63
71
|
end
|
64
72
|
|
65
|
-
|
73
|
+
# Force the table cache to be reread on the next request
|
74
|
+
# for tables.
|
75
|
+
#
|
76
|
+
def reset_cache
|
77
|
+
@@databases = nil
|
78
|
+
@@last_table_list = Hash.new{|h,k| h[k] = [Time.at(0), []]}
|
79
|
+
end
|
66
80
|
|
67
81
|
# Get the list of tables available (if a database is
|
68
82
|
# selected) at most once every ten seconds.
|
69
83
|
#
|
70
|
-
def tables(database
|
84
|
+
def tables(database=@@database_name)
|
71
85
|
now = Time.now
|
72
|
-
(last,
|
86
|
+
(last, tnames) = @@last_table_list[database]
|
73
87
|
if last + 10 < now
|
74
88
|
begin
|
75
89
|
if @@conn
|
76
|
-
if database && database != database_name
|
77
|
-
|
90
|
+
if database && database != @@database_name
|
91
|
+
tnames = @@conn.list_tables("FROM #{database}").sort
|
78
92
|
else
|
79
|
-
|
93
|
+
tnames = @@conn.list_tables.sort
|
80
94
|
end
|
81
95
|
end
|
82
96
|
rescue Mysql::Error => ex
|
83
|
-
|
97
|
+
tnames = []
|
84
98
|
end
|
85
|
-
@@last_table_list[database] = [now,
|
99
|
+
@@last_table_list[database] = [now, tnames]
|
86
100
|
end
|
87
|
-
|
101
|
+
tnames
|
88
102
|
end
|
89
103
|
|
90
104
|
# Provide a list of tab completions given the prompted
|
@@ -105,16 +119,16 @@ module RSQL
|
|
105
119
|
end
|
106
120
|
end
|
107
121
|
ret.compact!
|
108
|
-
|
122
|
+
else
|
123
|
+
ret = databases.select{|n| n != @@database_name && n.downcase.start_with?(str)}
|
124
|
+
if @@database_name
|
125
|
+
# if we've selected a db then we want to offer
|
126
|
+
# completions for other dbs as well as tables for
|
127
|
+
# the currently selected db
|
128
|
+
ret += tables.select{|n| n.downcase.start_with?(str)}
|
129
|
+
end
|
109
130
|
end
|
110
131
|
|
111
|
-
ret = databases.select{|n| n != database_name && n.downcase.start_with?(str)}
|
112
|
-
if database_name
|
113
|
-
# if we've selected a db then we want to offer
|
114
|
-
# completions for other dbs as well as tables for
|
115
|
-
# the currently selected db
|
116
|
-
ret += tables.select{|n| n.downcase.start_with?(str)}
|
117
|
-
end
|
118
132
|
return ret
|
119
133
|
end
|
120
134
|
|
@@ -181,6 +195,9 @@ module RSQL
|
|
181
195
|
|
182
196
|
end # class << self
|
183
197
|
|
198
|
+
# init the cache
|
199
|
+
reset_cache
|
200
|
+
|
184
201
|
########################################
|
185
202
|
|
186
203
|
def initialize(sql, elapsed, affected_rows,
|
@@ -230,11 +247,12 @@ module RSQL
|
|
230
247
|
# Get a row from the table hashed with the field names.
|
231
248
|
#
|
232
249
|
def [](index)
|
233
|
-
|
234
|
-
|
235
|
-
row = @table[index]
|
236
|
-
@fields.each_with_index {|f,i| hash[f.name] = row[i]}
|
250
|
+
if index < 0 || !@fields || !@table || @table.size <= index
|
251
|
+
return nil
|
237
252
|
end
|
253
|
+
hash = {}
|
254
|
+
row = @table[index]
|
255
|
+
@fields.each_with_index {|f,i| hash[f.name] = row[i]}
|
238
256
|
return hash
|
239
257
|
end
|
240
258
|
|
data/test/dummy_mysql.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'rubygems'
|
7
|
+
rescue LoadError
|
8
|
+
end
|
9
|
+
require 'mocha'
|
10
|
+
|
11
|
+
$: << File.expand_path(File.join(File.dirname(__FILE__),'..','lib')) << File.dirname(__FILE__)
|
12
|
+
require 'dummy_mysql.rb'
|
13
|
+
require 'rsql/mysql_results.rb'
|
14
|
+
require 'rsql/eval_context.rb'
|
15
|
+
require 'rsql/commands.rb'
|
16
|
+
|
17
|
+
class TestCommands < Test::Unit::TestCase
|
18
|
+
|
19
|
+
include RSQL
|
20
|
+
|
21
|
+
def setup
|
22
|
+
@orig_stdout = $stdout
|
23
|
+
$stdout = @strout = StringIO.new
|
24
|
+
@ctx = EvalContext.new
|
25
|
+
@conn = MySQLResults.conn = mock('Mysql')
|
26
|
+
end
|
27
|
+
|
28
|
+
def teardown
|
29
|
+
$stdout = @orig_stdout
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_simple_ruby
|
33
|
+
cmds = Commands.new('. puts :hello', :display_by_column)
|
34
|
+
assert_equal(false, cmds.empty?)
|
35
|
+
assert_not_nil(cmds.last)
|
36
|
+
cmds.run!(@ctx)
|
37
|
+
assert_equal('hello', @strout.string.chomp)
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_simple_sql
|
41
|
+
cmds = Commands.new('do some silly stuff', :display_by_column)
|
42
|
+
@conn.expects(:query).with(instance_of(String)).returns(nil)
|
43
|
+
@conn.expects(:affected_rows).returns(1)
|
44
|
+
cmds.run!(@ctx)
|
45
|
+
assert_match(/Query OK, 1 row affected/, @strout.string)
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_separators
|
49
|
+
cmds = Commands.new('. puts :hello\; puts :world;', :display_by_column)
|
50
|
+
cmds.run!(@ctx)
|
51
|
+
assert_equal('hello'+$/+'world', @strout.string.chomp)
|
52
|
+
|
53
|
+
# make sure our logic to handle eval'd blocks with args works
|
54
|
+
@strout.string = ''
|
55
|
+
cmds = Commands.new('. Proc.new{|a| p a} | @results.value.call(:fancy)', :display_by_column)
|
56
|
+
cmds.run!(@ctx)
|
57
|
+
assert_equal(':fancy', @strout.string.chomp)
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_multiple
|
61
|
+
@conn.expects(:query).with('one thing').returns(nil)
|
62
|
+
@conn.expects(:affected_rows).returns(1)
|
63
|
+
cmds = Commands.new('. "one thing" ; . p :hello', :display_by_column)
|
64
|
+
cmds.run!(@ctx)
|
65
|
+
assert_match(/^QueryOK,1rowaffected\(\d+.\d+sec\):hello$/,
|
66
|
+
@strout.string.gsub(/\s+/,''))
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_bangs
|
70
|
+
cmds = Commands.new('silly stuff ! this => that', :display_by_column)
|
71
|
+
@conn.expects(:query).with('silly stuff').returns(nil)
|
72
|
+
@conn.expects(:affected_rows).returns(13)
|
73
|
+
cmds.run!(@ctx)
|
74
|
+
assert_match(/Query OK, 13 rows affected/, @strout.string)
|
75
|
+
|
76
|
+
# now test logic to continue if it _doesn't_ look like a bang
|
77
|
+
cmds = Commands.new('silly stuff ! more things', :display_by_column)
|
78
|
+
@conn.expects(:query).with('silly stuff ! more things').returns(nil)
|
79
|
+
@conn.expects(:affected_rows).returns(4)
|
80
|
+
cmds.run!(@ctx)
|
81
|
+
assert_match(/Query OK, 4 rows affected/, @strout.string)
|
82
|
+
end
|
83
|
+
|
84
|
+
end # class TestCommands
|
@@ -0,0 +1,127 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
require 'tempfile'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'rubygems'
|
8
|
+
rescue LoadError
|
9
|
+
end
|
10
|
+
require 'mocha'
|
11
|
+
|
12
|
+
$: << File.expand_path(File.join(File.dirname(__FILE__),'..','lib')) << File.dirname(__FILE__)
|
13
|
+
require 'dummy_mysql.rb'
|
14
|
+
require 'rsql/mysql_results.rb'
|
15
|
+
require 'rsql/eval_context.rb'
|
16
|
+
|
17
|
+
class TestEvalContext < Test::Unit::TestCase
|
18
|
+
|
19
|
+
include RSQL
|
20
|
+
|
21
|
+
def setup
|
22
|
+
@orig_stdout = $stdout
|
23
|
+
$stdout = @strout = StringIO.new
|
24
|
+
@conn = MySQLResults.conn = mock('Mysql')
|
25
|
+
@conn.expects(:query).with(instance_of(String)).returns(nil)
|
26
|
+
@conn.expects(:affected_rows).returns(0)
|
27
|
+
@ctx = EvalContext.new
|
28
|
+
@ctx.load(File.join(File.dirname(__FILE__),'..','example.rsqlrc'))
|
29
|
+
end
|
30
|
+
|
31
|
+
def teardown
|
32
|
+
$stdout = @orig_stdout
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_load
|
36
|
+
@ctx.reload
|
37
|
+
assert_match(/loaded: .+?example.rsqlrc/, @strout.string)
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_eval
|
41
|
+
out = StringIO.new
|
42
|
+
|
43
|
+
# test a simple string registration
|
44
|
+
val = @ctx.safe_eval('cleanup_example', nil, out)
|
45
|
+
assert_equal('DROP TEMPORARY TABLE IF EXISTS rsql_example;', val)
|
46
|
+
assert_equal(true, out.string.empty?)
|
47
|
+
|
48
|
+
# test a block registration
|
49
|
+
val = @ctx.safe_eval('fill_table', nil, out)
|
50
|
+
assert_match(/(INSERT IGNORE INTO .+?){10}/, val)
|
51
|
+
assert_equal(true, out.string.empty?)
|
52
|
+
|
53
|
+
# test results handling and output redirection
|
54
|
+
res = mock
|
55
|
+
res.expects(:each_hash).yields({'value' => '2352'})
|
56
|
+
val = @ctx.safe_eval('to_report', res, out)
|
57
|
+
assert_equal(nil, val)
|
58
|
+
assert_equal("There are 1 small values and 0 big values.", out.string.chomp)
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_list
|
62
|
+
out = StringIO.new
|
63
|
+
val = @ctx.safe_eval('list', nil, out)
|
64
|
+
assert_match(/usage\s+description/, out.string)
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_complete
|
68
|
+
@conn.expects(:list_dbs).returns([])
|
69
|
+
assert_equal(17, @ctx.complete('').size)
|
70
|
+
assert_equal(['version'], @ctx.complete('v'))
|
71
|
+
assert_equal(['.version'], @ctx.complete('.v'))
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_bang_eval
|
75
|
+
@ctx.bangs = {'time' => :relative_time}
|
76
|
+
t = (Time.now - 2532435).to_s
|
77
|
+
assert_equal(t, @ctx.bang_eval('do no harm', t))
|
78
|
+
assert_equal(' 29 days ago', @ctx.bang_eval('time', t))
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_humanize
|
82
|
+
out = StringIO.new
|
83
|
+
assert_equal(' 9.16 GB',
|
84
|
+
@ctx.safe_eval('humanize_bytes(9832742324)', nil, out))
|
85
|
+
assert_equal(9835475108,
|
86
|
+
@ctx.safe_eval('dehumanize_bytes(" 9.16 GB")', nil, out))
|
87
|
+
assert_equal(' 20.9%',
|
88
|
+
@ctx.safe_eval('humanize_percentage(0.209384)', nil, out))
|
89
|
+
assert(out.string.empty?)
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_hex
|
93
|
+
bin = ''
|
94
|
+
100.times{|i| bin << i}
|
95
|
+
hex = @ctx.to_hexstr(bin)
|
96
|
+
assert_equal('0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f' <<
|
97
|
+
'... (68 bytes hidden)', hex)
|
98
|
+
|
99
|
+
out = StringIO.new
|
100
|
+
assert_equal('0x1234', @ctx.safe_eval('hexify("1234")', nil, out))
|
101
|
+
assert_equal('0x1234', @ctx.safe_eval('hexify("0x1234")', nil, out))
|
102
|
+
assert_equal('0x1234', @ctx.safe_eval('hexify(0x1234)', nil, out))
|
103
|
+
assert(out.string.empty?)
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_safe_save
|
107
|
+
out = StringIO.new
|
108
|
+
@ctx.safe_eval('@mystuff = {:one => 1, :two => 2}', nil, out)
|
109
|
+
tf = Tempfile.new('mystuff')
|
110
|
+
@ctx.safe_eval("safe_save(@mystuff, '#{tf.path}')", nil, out)
|
111
|
+
tf = tf.path + '.yml'
|
112
|
+
assert_equal("Saved: #{tf}", out.string.chomp)
|
113
|
+
assert_equal({:one => 1, :two => 2}, YAML.load_file(tf))
|
114
|
+
|
115
|
+
# now make sure it keeps one backup copy
|
116
|
+
out = StringIO.new
|
117
|
+
@ctx.safe_eval('@mystuff = {:one => 1}', nil, out)
|
118
|
+
@ctx.safe_eval("safe_save(@mystuff, '#{tf}')", nil, out)
|
119
|
+
assert_equal("Saved: #{tf}", out.string.chomp)
|
120
|
+
assert_equal({:one => 1}, YAML.load_file(tf))
|
121
|
+
assert_equal({:one => 1, :two => 2}, YAML.load_file(tf+'~'))
|
122
|
+
ensure
|
123
|
+
File.unlink(tf) if File.exists?(tf)
|
124
|
+
File.unlink(tf+'~') if File.exists?(tf+'~')
|
125
|
+
end
|
126
|
+
|
127
|
+
end # class TestEvalContext
|
@@ -0,0 +1,122 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'rubygems'
|
7
|
+
rescue LoadError
|
8
|
+
end
|
9
|
+
require 'mocha'
|
10
|
+
|
11
|
+
$: << File.expand_path(File.join(File.dirname(__FILE__),'..','lib')) << File.dirname(__FILE__)
|
12
|
+
require 'dummy_mysql.rb'
|
13
|
+
require 'rsql/mysql_results.rb'
|
14
|
+
|
15
|
+
class TestMySQLResults < Test::Unit::TestCase
|
16
|
+
|
17
|
+
include RSQL
|
18
|
+
|
19
|
+
def setup
|
20
|
+
MySQLResults.conn = nil
|
21
|
+
MySQLResults.database_name = nil
|
22
|
+
MySQLResults.reset_cache
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_databases
|
26
|
+
assert_equal(nil, MySQLResults.databases)
|
27
|
+
conn = mock('Mysql')
|
28
|
+
conn.expects(:list_dbs).returns(['accounts'])
|
29
|
+
MySQLResults.conn = conn
|
30
|
+
assert_equal(['accounts'], MySQLResults.databases)
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_tables
|
34
|
+
assert_equal([], MySQLResults.tables)
|
35
|
+
MySQLResults.reset_cache
|
36
|
+
|
37
|
+
conn = mock('Mysql')
|
38
|
+
conn.expects(:list_tables).returns(['users','groups'])
|
39
|
+
MySQLResults.conn = conn
|
40
|
+
assert_equal(['groups','users'], MySQLResults.tables)
|
41
|
+
MySQLResults.reset_cache
|
42
|
+
|
43
|
+
conn.expects(:list_tables).with(instance_of(String)).returns(['prefs'])
|
44
|
+
assert_equal(['prefs'], MySQLResults.tables('accounts'))
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_complete
|
48
|
+
assert_equal([], MySQLResults.complete(nil))
|
49
|
+
|
50
|
+
conn = mock('Mysql')
|
51
|
+
conn.expects(:list_dbs).returns(['accounts','devices','locations'])
|
52
|
+
MySQLResults.conn = conn
|
53
|
+
|
54
|
+
assert_equal(['accounts','devices','locations'], MySQLResults.complete(''))
|
55
|
+
assert_equal(['accounts'], MySQLResults.complete('a'))
|
56
|
+
|
57
|
+
MySQLResults.database_name = 'accounts'
|
58
|
+
conn.expects(:list_tables).returns(['prefs','names'])
|
59
|
+
assert_equal(['devices','locations','names','prefs'], MySQLResults.complete(''))
|
60
|
+
assert_equal(['names'], MySQLResults.complete('n'))
|
61
|
+
|
62
|
+
assert_equal(['accounts.names','accounts.prefs'], MySQLResults.complete('accounts.'))
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_query
|
66
|
+
f1 = mock('f1')
|
67
|
+
f1.expects(:name).returns('c1').times(12)
|
68
|
+
f1.expects(:type).returns(1).times(2)
|
69
|
+
f2 = mock('f2')
|
70
|
+
f2.expects(:name).returns('c2').times(11)
|
71
|
+
f2.expects(:type).returns(1).times(2)
|
72
|
+
|
73
|
+
res = mock('results')
|
74
|
+
res.expects(:num_rows).returns(2).times(2)
|
75
|
+
res.expects(:fetch_fields).returns([f1,f2])
|
76
|
+
|
77
|
+
rows = sequence(:rows)
|
78
|
+
res.expects(:fetch_row).in_sequence(rows).returns(['v1.1','v1.2'])
|
79
|
+
res.expects(:fetch_row).in_sequence(rows).returns(['v2.1','v2.2'])
|
80
|
+
res.expects(:fetch_row).in_sequence(rows).returns(nil)
|
81
|
+
|
82
|
+
conn = mock('Mysql')
|
83
|
+
conn.expects(:query).with(instance_of(String)).returns(res)
|
84
|
+
conn.expects(:affected_rows).returns(1)
|
85
|
+
MySQLResults.conn = conn
|
86
|
+
|
87
|
+
bangs = mock('bangs')
|
88
|
+
bangs.expects(:bang_eval).with(instance_of(String),instance_of(String)).
|
89
|
+
returns('val').times(4)
|
90
|
+
|
91
|
+
mres = MySQLResults.query('ignored', bangs)
|
92
|
+
assert_equal('ignored', mres.sql)
|
93
|
+
assert_equal(true, mres.any?)
|
94
|
+
assert_equal(false, mres.empty?)
|
95
|
+
assert_equal(2, mres.num_rows)
|
96
|
+
assert_equal({"c1"=>"val", "c2"=>"val"}, mres[0])
|
97
|
+
assert_equal({"c1"=>"val", "c2"=>"val"}, mres[1])
|
98
|
+
assert_equal(nil, mres[2])
|
99
|
+
|
100
|
+
cnt = 0
|
101
|
+
mres.each_hash do |row|
|
102
|
+
cnt += 1
|
103
|
+
assert_equal({"c1"=>"val", "c2"=>"val"}, row)
|
104
|
+
end
|
105
|
+
assert_equal(2, cnt)
|
106
|
+
|
107
|
+
dout = StringIO.new
|
108
|
+
mres.display_by_column(dout)
|
109
|
+
assert_match(/^c1c2--------valvalvalval--------2rowsinset/,
|
110
|
+
dout.string.gsub(/\s+/,''))
|
111
|
+
|
112
|
+
dout = StringIO.new
|
113
|
+
mres.display_by_batch(dout)
|
114
|
+
assert_equal('valvalvalval', dout.string.gsub(/\s+/,''))
|
115
|
+
|
116
|
+
dout = StringIO.new
|
117
|
+
mres.display_by_line(dout)
|
118
|
+
assert_match(/^\*+1.row\*+c1:valc2:val\*+2.row\*+c1:valc2:val2rowsinset/,
|
119
|
+
dout.string.gsub(/\s+/,''))
|
120
|
+
end
|
121
|
+
|
122
|
+
end # class TestMySQLResults
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rsql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 23
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 0.1.
|
9
|
+
- 6
|
10
|
+
version: 0.1.6
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Brad Robel-Forrest
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-05-
|
18
|
+
date: 2011-05-23 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: net-ssh
|
@@ -48,13 +48,16 @@ extra_rdoc_files:
|
|
48
48
|
files:
|
49
49
|
- LICENSE
|
50
50
|
- README.rdoc
|
51
|
-
- TODO
|
52
51
|
- bin/rsql
|
53
52
|
- example.rsqlrc
|
54
53
|
- lib/rsql.rb
|
55
54
|
- lib/rsql/commands.rb
|
56
55
|
- lib/rsql/eval_context.rb
|
57
56
|
- lib/rsql/mysql_results.rb
|
57
|
+
- test/dummy_mysql.rb
|
58
|
+
- test/test_commands.rb
|
59
|
+
- test/test_eval_context.rb
|
60
|
+
- test/test_mysql_results.rb
|
58
61
|
- lib/rsql/mysql.rb
|
59
62
|
homepage: https://github.com/bradrf/rsql
|
60
63
|
licenses: []
|
@@ -65,6 +68,8 @@ rdoc_options:
|
|
65
68
|
- RSQL Documentation
|
66
69
|
- --main
|
67
70
|
- README.rdoc
|
71
|
+
- --exclude
|
72
|
+
- mysql.rb
|
68
73
|
require_paths:
|
69
74
|
- lib
|
70
75
|
required_ruby_version: !ruby/object:Gem::Requirement
|
data/TODO
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
* Fix multiline dump to be columnar sensitive (i.e. all but the first
|
2
|
-
line will need a prefix of previous column length whitespace.
|
3
|
-
|
4
|
-
* Add option to allow registration to include an automatic displayer.
|
5
|
-
|
6
|
-
* Add ability to save passwords in local OS password storage.
|
7
|
-
|
8
|
-
* Add ability to highlight lines (e.g. to indicate warning lines about
|
9
|
-
some data like LastHeartbeatTime not recent).
|
10
|
-
|
11
|
-
* Add way to prevent this and just allow the raw string to be printed:
|
12
|
-
elsif HEX_RANGE.include?(field.type) && val =~ /[^[:print:]\s]/
|
13
|
-
val = @@eval_context.to_hexstr(val)
|
14
|
-
end
|
15
|
-
|
16
|
-
* Fix overlap of functionality between getting input in rsql and
|
17
|
-
parsing input in commands.rb (e.g. they both duplicate the concepts
|
18
|
-
of looking for exit/quit as well as what the end character(s) should
|
19
|
-
be)
|
20
|
-
|
21
|
-
* Wrap calling of mysql query in a thread to make it interruptable
|
22
|
-
(might need to re-establish a connection if it's stopped but
|
23
|
-
expected a reply...look at the source).
|
24
|
-
|
25
|
-
* Consider using mysql's ping to determine if we need to reconnect.
|
26
|
-
|
27
|
-
* Fix need for SSH password! It never asks.
|
28
|
-
|
29
|
-
* Find out if it's easy to point at source from the wiki to point
|
30
|
-
people at the example.rsqlrc.
|
31
|
-
|
32
|
-
* Consider adding the option to specify a dispalyer when registering a
|
33
|
-
recipe.
|
34
|
-
|
35
|
-
* Move everything in here into github issues.
|
36
|
-
|
37
|
-
* Consder renaming "register" to "recipe"
|