rsql 0.1.5 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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_ values may also provide _user_ and
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
- It is possible to provide empty passwords by simply having nothing
54
- listed between demarcation points:
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
- == EXAMPLE
75
+ https://github.com/bradrf/rsql/raw/master/example.rsqlrc
62
76
 
63
- Try walking through link:../example.rsqlrc.
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
- require 'net/ssh'
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 = get_password('mysql password? ') unless 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
- db_name = ARGV.shift
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
- opts[:password] = ssh_password if ssh_password
215
- ssh = Net::SSH.start(ssh_host, ssh_user, opts)
216
- ssh_enabled = true
217
- ssh.forward.local(mysql_port, mysql_host, 3306)
218
- port_opened = true
219
- ssh.loop(1) { ssh_enabled }
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} ssh host in 15 seconds"
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, db_name, mysql_port)
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
- db_name = (MySQLResults.database_name || db_name)
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
@@ -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}
@@ -2,7 +2,7 @@
2
2
  # Commands using an EvalContext for handling recipes.
3
3
  #
4
4
  module RSQL
5
- VERSION = '0.1.5'
5
+ VERSION = '0.1.6'
6
6
 
7
7
  require 'rsql/mysql'
8
8
  require 'rsql/mysql_results'
@@ -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 match =~ /\{\s*|do\s*/
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, @default_displayer)
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 MysqlError => ex
217
+ rescue Mysql::Error => ex
216
218
  $stderr.puts ex.message
217
219
  rescue Exception => ex
218
220
  $stderr.puts ex.inspect
@@ -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:
@@ -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
- @@databases ||= @@conn.list_dbs.sort if @@conn
67
+ if @@conn && @@databases.nil?
68
+ @@databases = @@conn.list_dbs.sort
69
+ end
70
+ @@databases
63
71
  end
64
72
 
65
- @@last_table_list = Hash.new{|h,k| h[k] = [Time.at(0), []]}
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 = nil)
84
+ def tables(database=@@database_name)
71
85
  now = Time.now
72
- (last, tables) = @@last_table_list[database]
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
- tables = @@conn.list_tables("FROM #{database}").sort
90
+ if database && database != @@database_name
91
+ tnames = @@conn.list_tables("FROM #{database}").sort
78
92
  else
79
- tables = @@conn.list_tables.sort
93
+ tnames = @@conn.list_tables.sort
80
94
  end
81
95
  end
82
96
  rescue Mysql::Error => ex
83
- tables = []
97
+ tnames = []
84
98
  end
85
- @@last_table_list[database] = [now, tables]
99
+ @@last_table_list[database] = [now, tnames]
86
100
  end
87
- tables
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
- return ret
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
- hash = {}
234
- if @fields && @table
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
 
@@ -0,0 +1,11 @@
1
+ # Dummy up some MySQL constants.
2
+ module RSQL
3
+ class Mysql
4
+ class Error < Exception
5
+ end
6
+ class Field
7
+ TYPE_TINY_BLOB = 1
8
+ TYPE_STRING = 2
9
+ end
10
+ end
11
+ end
@@ -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: 17
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 5
10
- version: 0.1.5
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-16 00:00:00 Z
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"