rsql 0.1.5 → 0.1.6
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.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"
|