rsql 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2011 by brad+rsql@gigglewax.com
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.txt ADDED
@@ -0,0 +1,119 @@
1
+ = rsql
2
+
3
+ https://github.com/bradrf/rsql
4
+
5
+ == DESCRIPTION
6
+
7
+ This is an application to make working with a SQL command line more
8
+ convenient by allowing interaction with Ruby in addition to embedding
9
+ the common operation of using a SSH connection to an intermediary host
10
+ for access to the SQL server.
11
+
12
+ == SYNOPSIS
13
+
14
+ See rsql -help for usage.
15
+
16
+ Aside from the standard MySQL command syntax, the following
17
+ functionality allows for a little more expressive processing.
18
+
19
+ Multiple commands can be issued in one set by separation with
20
+ semicolons.
21
+
22
+ Generating SQL
23
+ --------------
24
+
25
+ Ruby code may be called to generate the SQL that is to be executed.
26
+ This is done by starting any command string with a period. If the
27
+ final result of evaluating the command string is another string, it is
28
+ executed as SQL. Any semicolons meant to be processed by Ruby must be
29
+ escaped. Example:
30
+
31
+ rsql> . puts 'hello world!' \\; 'select * from Account'
32
+
33
+ Utilizing Canned Methods
34
+ ------------------------
35
+
36
+ Commands can be stored in the .rsqlrc file in your HOME directory to
37
+ expose methods that may be invoked to generate SQL with variable
38
+ interpolation. Use of the 'register' helper is recommended for this
39
+ approach. These can then be called in the same way as above. Example:
40
+
41
+ In the .rsqlrc file...
42
+
43
+ register :users_by_email, :email %q{
44
+ SELECT * FROM Users WHERE email = '\#\{email\}'
45
+ }
46
+
47
+ ...then from the prompt:
48
+
49
+ rsql> . users_by_email 'brad@gigglewax.com'
50
+
51
+ If a block is provided to the registration, it will be called as a
52
+ method. Example:
53
+
54
+ In the .sqlrc file...
55
+
56
+ register :dumby, :hello do |*args|
57
+ p args
58
+ end
59
+
60
+ rsql> . dumby :world
61
+
62
+ All registered methods can be listed using the built-in 'list'
63
+ command.
64
+
65
+ Changes to a sourced file can be reloaded using the built-in 'reload'
66
+ command.
67
+
68
+ Processing Column Data
69
+ ----------------------
70
+
71
+ Ruby can be called to process any data on a per-column basis before a
72
+ displayer is used to render the output. In this way, one can write
73
+ Ruby to act like MySQL functions on all the data for a given column,
74
+ converting it into a more readable value. A bang indicator (exlamation
75
+ point: !) is used to demarcate a mapping of column names to Ruby
76
+ methods that should be invoked to processes content. Example:
77
+
78
+ rsql> select IpAddress from Devices ; ! IpAddress => bin_to_str
79
+
80
+ This will call 'bin_to_str' for each 'IpAddress' returned from the
81
+ query. Mulitple mappings are separated by a comma. These mappings can
82
+ also be utilized in a canned method. Example:
83
+
84
+ register :all_ips, 'select IpAddress from Devices', 'IpAddress' => :bin_to_str
85
+
86
+ Redirection
87
+ -----------
88
+
89
+ Output from one or more queries may be post-processed dynamically. If
90
+ any set of commands is follwed by a greater-than symbol, all results
91
+ will be stored in a global $results array (with field information
92
+ stored in $fields) and the final Ruby code will be evaluated with
93
+ access to them. Any result of the evaluation that is a string is then
94
+ executed as SQL. Example:
95
+
96
+ rsql> select * from Account; select * from Users; > $results.each {|r| p r}
97
+
98
+ == LICENSE
99
+
100
+ Copyright (C) 2011 by Brad Robel-Forrest <brad+rsql@gigglewax.com>
101
+
102
+ Permission is hereby granted, free of charge, to any person obtaining
103
+ a copy of this software and associated documentation files (the
104
+ "Software"), to deal in the Software without restriction, including
105
+ without limitation the rights to use, copy, modify, merge, publish,
106
+ distribute, sublicense, and/or sell copies of the Software, and to
107
+ permit persons to whom the Software is furnished to do so, subject to
108
+ the following conditions:
109
+
110
+ The above copyright notice and this permission notice shall be
111
+ included in all copies or substantial portions of the Software.
112
+
113
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
114
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
115
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
116
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
117
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
118
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
119
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/TODO ADDED
@@ -0,0 +1,30 @@
1
+ * Support separation of semicolon content from registered sql.
2
+
3
+ * Fix multiline dump to be columnar sensitive (i.e. all but the first
4
+ line will need a prefix of previous column length whitespace.
5
+
6
+ * Add option to allow registration to include an automatic displayer.
7
+
8
+ * Add ability to save passwords in local OS password storage.
9
+
10
+ * Add ability to highlight lines (e.g. to indicate warning lines about
11
+ some data like LastHeartbeatTime not recent).
12
+
13
+ * Add way to prevent this and just allow the raw string to be printed:
14
+ elsif HEX_RANGE.include?(field.type) && val =~ /[^[:print:]\s]/
15
+ val = @@eval_context.to_hexstr(val)
16
+ end
17
+
18
+ * Fix overlap of functionality between getting input in rsql and
19
+ parsing input in commands.rb (e.g. they both duplicate the concepts
20
+ of looking for exit/quit as well as what the end character(s) should
21
+ be)
22
+
23
+ * Wrap calling of mysql query in a thread to make it interruptable
24
+ (might need to re-establish a connection if it's stopped but
25
+ expected a reply...look at the source).
26
+
27
+ * Consider using mysql's ping to determine if we need to reconnect.
28
+
29
+ * Move last_command logic into mysql_results and remember last five
30
+ (or so).
data/bin/rsql ADDED
@@ -0,0 +1,357 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Copyright (C) 2011 by Brad Robel-Forrest <brad+rsql@gigglewax.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ begin
24
+ # this isn't required unless that's how mysql and net/ssh have
25
+ # been installed
26
+ require 'rubygems'
27
+ rescue LoadError
28
+ end
29
+
30
+ require 'thread'
31
+ require 'timeout'
32
+ require 'readline'
33
+ require 'yaml'
34
+ require 'net/ssh'
35
+
36
+ # allow ourselves to be run from within a source tree
37
+ if File.symlink?(__FILE__)
38
+ fn = File.readlink(__FILE__)
39
+ else
40
+ fn = __FILE__
41
+ end
42
+ libdir = File.expand_path(File.join(File.dirname(fn),'..','lib'))
43
+ $: << libdir if File.directory?(libdir)
44
+
45
+ require 'rsql'
46
+ include RSQL
47
+
48
+ bn = File.basename($0, '.rb')
49
+
50
+ eval_context = EvalContext.new
51
+
52
+ if i = ARGV.index('-rc')
53
+ ARGV.delete_at(i)
54
+ rc_fn = ARGV.delete_at(i)
55
+ else
56
+ rc_fn = File.join(ENV['HOME'], ".#{bn}rc")
57
+ end
58
+
59
+ eval_context.load(rc_fn) if File.exists?(rc_fn)
60
+
61
+ def get_password(prompt)
62
+ iswin = nil != (RUBY_PLATFORM =~ /(win|w)32$/)
63
+ STDOUT.print(prompt)
64
+ STDOUT.flush
65
+ `stty -echo` unless iswin
66
+ password = STDIN.gets
67
+ password.chomp!
68
+ ensure
69
+ `stty echo` unless iswin
70
+ STDOUT.puts
71
+ return password
72
+ end
73
+
74
+ # safely separate login credentials while preserving "emtpy" values--
75
+ # anything of the form [<username>[:<password]@]<host>
76
+ #
77
+ def split_login(str)
78
+ login = []
79
+ if i = str.rindex(?@)
80
+ login << str[i+1..-1]
81
+ if 0 < i
82
+ str = str[0..i-1]
83
+ i = str.index(?:)
84
+ if 0 == i
85
+ login << '' << str[i+1..-1]
86
+ elsif i
87
+ login << str[0..i-1] << str[i+1..-1]
88
+ else
89
+ login << str
90
+ end
91
+ else
92
+ login << ''
93
+ end
94
+ else
95
+ login << str
96
+ end
97
+ end
98
+
99
+ if ARGV.delete('-help')
100
+ eval_context.help
101
+ exit
102
+ end
103
+
104
+ if ARGV.delete('-version')
105
+ puts "#{bn} v#{RSQL::VERSION}"
106
+ exit
107
+ end
108
+
109
+ if i = ARGV.index('-maxrows')
110
+ ARGV.delete_at(i)
111
+ MySQLResults.max_rows = ARGV.delete_at(i).to_i
112
+ end
113
+
114
+ if i = ARGV.index('-batch')
115
+ ARGV.delete_at(i)
116
+ MySQLResults.field_separator = ARGV.delete_at(i)
117
+ MySQLResults.field_separator = "\t" if MySQLResults.field_separator == '\t'
118
+ batch_output = true
119
+ end
120
+
121
+ if i = ARGV.index('-ssh')
122
+ ARGV.delete_at(i)
123
+ (ssh_host, ssh_user, ssh_password) = split_login(ARGV.delete_at(i))
124
+ end
125
+
126
+ if i = ARGV.index('-e')
127
+ ARGV.delete_at(i)
128
+ batch_input = ''
129
+ ARGV.delete_if do |arg|
130
+ arg_i = ARGV.index(arg)
131
+ if i <= arg_i
132
+ batch_input << ' ' << arg
133
+ end
134
+ end
135
+ batch_input.strip!
136
+ end
137
+
138
+ if ARGV.size < 1
139
+ prefix = ' ' << ' ' * bn.size
140
+ $stderr.puts <<USAGE
141
+
142
+ usage: #{bn} [-version] [-help]
143
+ #{prefix}[-rc <rcfile>] [-maxrows <max>] [-batch <field_separator>]
144
+ #{prefix}[-ssh [<ssh_user>[:<ssh_password>]@]<ssh_host>]
145
+ #{prefix}[<mysql_user>[:<mysql_password>]@]<mysql_host>
146
+ #{prefix}[<database>] [-e <remaining_args_as_input>]
147
+
148
+ If -ssh is used, a SSH tunnel is established before trying to
149
+ connect to the MySQL server.
150
+
151
+ Commands may either be passed in non-interactively via the -e option
152
+ or by piping into the process' standard input (stdin). If using -e,
153
+ it _must_ be the last option following all other arguments.
154
+
155
+ USAGE
156
+ exit 1
157
+ end
158
+
159
+ (mysql_host, mysql_user, mysql_password) = split_login(ARGV.shift)
160
+ mysql_password = get_password('mysql password? ') unless mysql_password
161
+ real_mysql_host = mysql_host
162
+
163
+ if ssh_host
164
+ # randomly pick a tcp port above 1024
165
+ mysql_port = rand(0xffff-1025) + 1025
166
+ else
167
+ mysql_port = 3306
168
+ end
169
+
170
+ db_name = ARGV.shift
171
+
172
+ unless $stdin.tty?
173
+ batch_input = $stdin.read.gsub(/\r?\n/,';')
174
+ end
175
+
176
+ # make sure we remove any duplicates when we add to the history to
177
+ # keep it clean
178
+ #
179
+ def add_to_history(item)
180
+ found = nil
181
+ Readline::HISTORY.each_with_index do |h,i|
182
+ if h == item
183
+ found = i
184
+ break
185
+ end
186
+ end
187
+ Readline::HISTORY.delete_at(found) if found
188
+ Readline::HISTORY.push(item)
189
+ end
190
+
191
+ # try closing but wrapped with a timer so we don't hang forever
192
+ #
193
+ def safe_timeout(conn, meth, name)
194
+ Timeout.timeout(5) { conn.send(meth) }
195
+ true
196
+ rescue Timeout::Error
197
+ $stderr.puts "Timed out waiting to close #{name} connection"
198
+ false
199
+ rescue Exception => ex
200
+ $stderr.puts(ex)
201
+ false
202
+ end
203
+
204
+ MySQLResults.max_rows ||= batch_output ? 5000 : 1000
205
+
206
+ ssh_enabled = false
207
+
208
+ if ssh_host
209
+
210
+ # might need to open an idle channel here so server doesn't close on
211
+ # us...or just loop reconnection here in the thread...
212
+
213
+ port_opened = false
214
+
215
+ puts "SSH #{ssh_user}#{ssh_user ? '@' : ''}#{ssh_host}..." unless batch_input
216
+ ssh = nil
217
+ ssh_thread = Thread.new do
218
+ opts = {:timeout => 15}
219
+ opts[:password] = ssh_password if ssh_password
220
+ ssh = Net::SSH.start(ssh_host, ssh_user, opts)
221
+ ssh_enabled = true
222
+ ssh.forward.local(mysql_port, mysql_host, 3306)
223
+ port_opened = true
224
+ ssh.loop(1) { ssh_enabled }
225
+ end
226
+
227
+ 15.times do
228
+ break if ssh_enabled && port_opened
229
+ sleep(1)
230
+ end
231
+
232
+ unless ssh_enabled
233
+ $stderr.puts "failed to connect to #{ssh_host} ssh host in 15 seconds"
234
+ exit 1
235
+ end
236
+
237
+ unless port_opened
238
+ $stderr.puts("failed to forward #{mysql_port}:#{mysql_host}:3306 via #{ssh_host} " \
239
+ "ssh host in 15 seconds")
240
+ exit 1
241
+ end
242
+
243
+ # now have our mysql connection use our port forward...
244
+ mysql_host = '127.0.0.1'
245
+ end
246
+
247
+ puts "MySQL #{mysql_user}@#{real_mysql_host}..." unless batch_input
248
+ begin
249
+ MySQLResults.conn = Mysql.new(mysql_host, mysql_user, mysql_password, db_name, mysql_port)
250
+ rescue Mysql::Error => ex
251
+ if ex.message.include?('Client does not support authentication')
252
+ $stderr.puts "failed to connect to #{mysql_host} mysql server: unknown credentials?"
253
+ else
254
+ $stderr.puts "failed to connect to #{mysql_host} mysql server: #{ex.message}"
255
+ end
256
+ exit 1
257
+ end
258
+
259
+ eval_context.call_init_registrations(MySQLResults.conn)
260
+
261
+ history_fn = File.join(ENV['HOME'], ".#{bn}_history")
262
+ if File.exists?(history_fn) && 0 < File.size(history_fn)
263
+ YAML.load_file(history_fn).each {|i| Readline::HISTORY.push(i)}
264
+ end
265
+
266
+ Readline.completion_proc = eval_context.method(:complete)
267
+
268
+ cmd_thread = Thread.new do
269
+ me = Thread.current
270
+ me[:shutdown] = false
271
+ until me[:shutdown] do
272
+ default_displayer = :display_by_column
273
+ if batch_input
274
+ default_displayer = :display_by_batch if batch_output
275
+ me[:shutdown] = true # only run once
276
+ input = batch_input
277
+ else
278
+ db_name = (MySQLResults.database_name || db_name)
279
+ puts '',"[#{mysql_user}@#{ssh_host||mysql_host}:#{db_name}]"
280
+ input = ''
281
+ prompt = bn + '> '
282
+ loop do
283
+ str = Readline.readline(prompt)
284
+ if str.nil?
285
+ input = nil if input.empty?
286
+ break
287
+ end
288
+ if str =~ /^\s*(exit|quit)\s*$/
289
+ me[:shutdown] = true
290
+ break
291
+ end
292
+ input << str
293
+ break if input =~ /([^\\];|\\G)\s*$/
294
+ # make sure we separate the lines with some whitespace if
295
+ # they didn't
296
+ input << ' ' unless str =~ /\s$/
297
+ prompt = ''
298
+ end
299
+ if input.nil? || me[:shutdown]
300
+ puts
301
+ break
302
+ end
303
+ input.strip!
304
+ next if input.empty?
305
+ end
306
+
307
+ add_to_history(input)
308
+ cmds = Commands.new(input, default_displayer)
309
+ if cmds.empty?
310
+ Readline::HISTORY.pop
311
+ next
312
+ end
313
+
314
+ me[:running] = true
315
+ break if cmds.run!(eval_context) == :done
316
+ me[:running] = false
317
+ end
318
+ end
319
+
320
+ Signal.trap('INT') do
321
+ if cmd_thread[:running] && MySQLResults.conn
322
+ $stderr.puts 'Interrupting MySQL query...'
323
+ safe_timeout(MySQLResults.conn, :close, 'MySQL')
324
+ MySQLResults.conn = nil
325
+ end
326
+ $stderr.puts 'Shutting down...'
327
+ cmd_thread[:shutdown] = true
328
+ sleep(0.3)
329
+ cmd_thread.kill
330
+ end
331
+
332
+ begin
333
+ cmd_thread.join
334
+ rescue Exception => ex
335
+ $stderr.puts ex.message, ex.backtrace
336
+ end
337
+
338
+ unless MySQLResults.conn.nil?
339
+ safe_timeout(MySQLResults.conn, :close, 'MySQL')
340
+ MySQLResults.conn = nil
341
+ end
342
+
343
+ sleep(0.3)
344
+ ssh_enabled = false
345
+
346
+ if Readline::HISTORY.any?
347
+ if 100 < Readline::HISTORY.size
348
+ (Readline::HISTORY.size - 100).times do |i|
349
+ Readline::HISTORY.delete_at(i)
350
+ end
351
+ end
352
+ File.open(history_fn, 'w') {|f| YAML.dump(Readline::HISTORY.to_a, f)}
353
+ end
354
+
355
+ if ssh_thread
356
+ safe_timeout(ssh_thread, :join, 'SSH')
357
+ end