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.
@@ -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"