rsql 0.1.3
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/LICENSE +19 -0
- data/README.txt +119 -0
- data/TODO +30 -0
- data/bin/rsql +357 -0
- data/lib/rsql/commands.rb +219 -0
- data/lib/rsql/eval_context.rb +414 -0
- data/lib/rsql/mysql.rb +1127 -0
- data/lib/rsql/mysql_results.rb +320 -0
- data/lib/rsql.rb +8 -0
- metadata +94 -0
@@ -0,0 +1,320 @@
|
|
1
|
+
# Copyright (C) 2011 by Brad Robel-Forrest <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.
|
20
|
+
|
21
|
+
module RSQL
|
22
|
+
|
23
|
+
########################################
|
24
|
+
# A wrapper to make it easier to work with MySQL results (and prettier)
|
25
|
+
#
|
26
|
+
class MySQLResults
|
27
|
+
|
28
|
+
HEX_RANGE = (Mysql::Field::TYPE_TINY_BLOB..Mysql::Field::TYPE_STRING)
|
29
|
+
|
30
|
+
@@conn = nil
|
31
|
+
@@field_separator = ' '
|
32
|
+
@@max_rows = 1000
|
33
|
+
@@database_name = nil
|
34
|
+
|
35
|
+
class MaxRowsException < RangeError
|
36
|
+
def initialize(rows, max)
|
37
|
+
@rows = rows
|
38
|
+
@max = max
|
39
|
+
end
|
40
|
+
attr_reader :rows, :max
|
41
|
+
end
|
42
|
+
|
43
|
+
class << self
|
44
|
+
|
45
|
+
def conn; @@conn; end
|
46
|
+
def conn=(conn); @@conn = conn; end
|
47
|
+
|
48
|
+
def field_separator; @@field_separator; end
|
49
|
+
def field_separator=(sep); @@field_separator = sep; end
|
50
|
+
|
51
|
+
def max_rows; @@max_rows; end
|
52
|
+
def max_rows=(cnt); @@max_rows = cnt; end
|
53
|
+
|
54
|
+
# get the name of the current database in use
|
55
|
+
#
|
56
|
+
def database_name; @@database_name; end
|
57
|
+
|
58
|
+
# get the list of databases available
|
59
|
+
#
|
60
|
+
def databases
|
61
|
+
@@databases ||= @@conn.list_dbs.sort if @@conn
|
62
|
+
end
|
63
|
+
|
64
|
+
# get the list of tables available (if a database is
|
65
|
+
# selected) at most once every ten seconds
|
66
|
+
#
|
67
|
+
@@last_table_list = Hash.new{|h,k| h[k] = [Time.at(0), []]}
|
68
|
+
def tables(database = nil)
|
69
|
+
now = Time.now
|
70
|
+
(last, tables) = @@last_table_list[database]
|
71
|
+
if last + 10 < now
|
72
|
+
begin
|
73
|
+
if @@conn
|
74
|
+
if database && database != database_name
|
75
|
+
tables = @@conn.list_tables("FROM #{database}").sort
|
76
|
+
else
|
77
|
+
tables = @@conn.list_tables.sort
|
78
|
+
end
|
79
|
+
end
|
80
|
+
rescue Mysql::Error => ex
|
81
|
+
tables = []
|
82
|
+
end
|
83
|
+
@@last_table_list[database] = [now, tables]
|
84
|
+
end
|
85
|
+
tables
|
86
|
+
end
|
87
|
+
|
88
|
+
# provide a list of tab completions given the prompted
|
89
|
+
# value
|
90
|
+
#
|
91
|
+
def complete(str)
|
92
|
+
return [] unless @@conn
|
93
|
+
|
94
|
+
# offer table names from a specific database
|
95
|
+
if str =~ /^([^.]+)\.(.*)$/
|
96
|
+
db = $1
|
97
|
+
tb = $2
|
98
|
+
ret = tables(db).collect do |n|
|
99
|
+
if n.downcase.start_with?(tb)
|
100
|
+
"#{db}.#{n}"
|
101
|
+
else
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
end
|
105
|
+
ret.compact!
|
106
|
+
return ret
|
107
|
+
end
|
108
|
+
|
109
|
+
ret = databases.select{|n| n != database_name && n.downcase.start_with?(str)}
|
110
|
+
if database_name
|
111
|
+
# if we've selected a db then we want to offer
|
112
|
+
# completions for other dbs as well as tables for
|
113
|
+
# the currently selected db
|
114
|
+
ret += tables.select{|n| n.downcase.start_with?(str)}
|
115
|
+
end
|
116
|
+
return ret
|
117
|
+
end
|
118
|
+
|
119
|
+
# get results from a query
|
120
|
+
#
|
121
|
+
def query(sql, eval_context, raw=false, max_rows=@@max_rows)
|
122
|
+
start = Time.now.to_f
|
123
|
+
results = @@conn.query(sql)
|
124
|
+
elapsed = Time.now.to_f - start.to_f
|
125
|
+
|
126
|
+
affected_rows = @@conn.affected_rows
|
127
|
+
unless results && 0 < results.num_rows
|
128
|
+
return new(sql, elapsed, affected_rows)
|
129
|
+
end
|
130
|
+
|
131
|
+
if max_rows < results.num_rows
|
132
|
+
raise MaxRowsException.new(results.num_rows, max_rows)
|
133
|
+
end
|
134
|
+
|
135
|
+
# extract mysql results into our own table so we can predetermine the
|
136
|
+
# lengths of columns and give users a chance to reformat column data
|
137
|
+
# before it's displayed (via the bang maps)
|
138
|
+
|
139
|
+
fields = results.fetch_fields
|
140
|
+
fields.collect! do |field|
|
141
|
+
def field.longest_length=(len); @longest_length = len; end
|
142
|
+
def field.longest_length; @longest_length; end
|
143
|
+
field.longest_length = field.name.length
|
144
|
+
field
|
145
|
+
end
|
146
|
+
|
147
|
+
results_table = []
|
148
|
+
while vals = results.fetch_row
|
149
|
+
row = []
|
150
|
+
fields.each_with_index do |field, i|
|
151
|
+
val = eval_context.bang_eval(field.name, vals[i])
|
152
|
+
unless raw
|
153
|
+
if val.nil?
|
154
|
+
val = 'NULL'
|
155
|
+
elsif HEX_RANGE.include?(field.type) && val =~ /[^[:print:]\s]/
|
156
|
+
val = eval_context.to_hexstr(val)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
vlen = val.respond_to?(:length) ? val.length : 0
|
160
|
+
if field.longest_length < vlen
|
161
|
+
if String === val
|
162
|
+
# consider only the longest line length since some
|
163
|
+
# output contains multiple lines like "show create table"
|
164
|
+
longest_line = val.split(/\r?\n/).collect{|l|l.length}.max
|
165
|
+
if field.longest_length < longest_line
|
166
|
+
field.longest_length = longest_line
|
167
|
+
end
|
168
|
+
else
|
169
|
+
field.longest_length = val.length
|
170
|
+
end
|
171
|
+
end
|
172
|
+
row << val
|
173
|
+
end
|
174
|
+
results_table << row
|
175
|
+
end
|
176
|
+
|
177
|
+
return new(sql, elapsed, affected_rows, fields, results_table)
|
178
|
+
end
|
179
|
+
|
180
|
+
end # class << self
|
181
|
+
|
182
|
+
########################################
|
183
|
+
|
184
|
+
def initialize(sql, elapsed, affected_rows,
|
185
|
+
fields=nil, table=nil, field_separator=@@field_separator)
|
186
|
+
@sql = sql;
|
187
|
+
@elapsed = elapsed;
|
188
|
+
@affected_rows = affected_rows;
|
189
|
+
@fields = fields
|
190
|
+
@table = table
|
191
|
+
@field_separator = field_separator
|
192
|
+
|
193
|
+
# we set this here so that (a) it occurs _after_ we are
|
194
|
+
# successful and so we can show an appropriate messge in a
|
195
|
+
# displayer
|
196
|
+
if @sql.match(/use\s+(\S+)/)
|
197
|
+
@database_changed = true
|
198
|
+
@@database_name = $1
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# get the number of rows that were affected by the query
|
203
|
+
#
|
204
|
+
attr_reader :sql, :affected_rows
|
205
|
+
|
206
|
+
# determine if there are any results
|
207
|
+
#
|
208
|
+
def any?
|
209
|
+
!@table.nil?
|
210
|
+
end
|
211
|
+
|
212
|
+
# determine if there are no results
|
213
|
+
#
|
214
|
+
def empty?
|
215
|
+
@table.nil?
|
216
|
+
end
|
217
|
+
|
218
|
+
# get the number of rows available in the results
|
219
|
+
#
|
220
|
+
def num_rows
|
221
|
+
@table ? @table.size : 0
|
222
|
+
end
|
223
|
+
|
224
|
+
# get a row from the table hashed with the field names
|
225
|
+
#
|
226
|
+
def row_hash(index)
|
227
|
+
hash = {}
|
228
|
+
if @fields && @table
|
229
|
+
row = @table[index]
|
230
|
+
@fields.each_with_index {|f,i| hash[f.name] = row[i]}
|
231
|
+
end
|
232
|
+
return hash
|
233
|
+
end
|
234
|
+
|
235
|
+
# iterate through each row of the table hashed with the field
|
236
|
+
# names
|
237
|
+
#
|
238
|
+
def each_hash(&block)
|
239
|
+
if @table
|
240
|
+
@table.each do |row|
|
241
|
+
hash = {}
|
242
|
+
@fields.each_with_index {|f,i| hash[f.name] = row[i]}
|
243
|
+
yield(hash)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# show a set of results in a decent fashion
|
249
|
+
#
|
250
|
+
def display_by_column(io=$stdout)
|
251
|
+
if @fields && @table
|
252
|
+
fmts = []
|
253
|
+
names = []
|
254
|
+
len = 0
|
255
|
+
@fields.each do |field|
|
256
|
+
fmts << "%-#{field.longest_length}s"
|
257
|
+
names << field.name
|
258
|
+
len += field.longest_length
|
259
|
+
end
|
260
|
+
|
261
|
+
fmt = fmts.join(@field_separator)
|
262
|
+
sep = '-' * (len + fmts.length)
|
263
|
+
io.puts(fmt % names, sep)
|
264
|
+
@table.each{|row| io.puts(fmt % row)}
|
265
|
+
display_stats(io, sep)
|
266
|
+
else
|
267
|
+
display_stats(io)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# show a set of results with a single character separation
|
272
|
+
#
|
273
|
+
def display_by_batch(io=$stdout)
|
274
|
+
if @fields && @table
|
275
|
+
fmt = (['%s'] * @fields.size).join(@field_separator)
|
276
|
+
@table.each{|row| io.puts(fmt % row)}
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
# show a set of results line separated
|
281
|
+
#
|
282
|
+
def display_by_line(io=$stdout)
|
283
|
+
if @fields && @table
|
284
|
+
namelen = 0
|
285
|
+
@fields.each do |field|
|
286
|
+
namelen = field.name.length if namelen < field.name.length
|
287
|
+
end
|
288
|
+
namelen += 1
|
289
|
+
|
290
|
+
@table.each_with_index do |row, i|
|
291
|
+
io.puts("#{'*'*30} #{i+1}. row #{'*'*30}")
|
292
|
+
row.each_with_index do |val, vi|
|
293
|
+
io.printf("%#{namelen}s #{val}#{$/}", @fields[vi].name + ':')
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
display_stats(io)
|
298
|
+
end
|
299
|
+
|
300
|
+
def display_stats(io=$stdout, hdr='')
|
301
|
+
if @table
|
302
|
+
if @database_changed
|
303
|
+
io.puts(hdr, "Database changed");
|
304
|
+
hdr = ''
|
305
|
+
end
|
306
|
+
s = 1 == @table.size ? 'row' : 'rows'
|
307
|
+
io.puts(hdr, "#{@table.size} #{s} in set (#{'%0.2f'%@elapsed} sec)")
|
308
|
+
else
|
309
|
+
if @database_changed
|
310
|
+
io.puts(hdr, "Database changed");
|
311
|
+
else
|
312
|
+
s = 1 == @affected_rows ? 'row' : 'rows'
|
313
|
+
io.puts(hdr, "Query OK, #{@affected_rows} #{s} affected (#{'%0.2f'%@elapsed} sec)")
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
end # class MySQLResults
|
319
|
+
|
320
|
+
end # module RSQL
|
data/lib/rsql.rb
ADDED
metadata
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rsql
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 3
|
10
|
+
version: 0.1.3
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Brad Robel-Forrest
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-05-13 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: net-ssh
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 11
|
29
|
+
segments:
|
30
|
+
- 2
|
31
|
+
- 1
|
32
|
+
- 0
|
33
|
+
version: 2.1.0
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
description: |
|
37
|
+
Rsql makes working with a MySQL command line more convenient through
|
38
|
+
the use of recipes and embedding the common operation of using a SSH
|
39
|
+
connection to an intermediary host for access to the MySQL server.
|
40
|
+
|
41
|
+
email: brad+rsql@gigglewax.com
|
42
|
+
executables: []
|
43
|
+
|
44
|
+
extensions: []
|
45
|
+
|
46
|
+
extra_rdoc_files: []
|
47
|
+
|
48
|
+
files:
|
49
|
+
- LICENSE
|
50
|
+
- README.txt
|
51
|
+
- TODO
|
52
|
+
- bin/rsql
|
53
|
+
- lib/rsql.rb
|
54
|
+
- lib/rsql/commands.rb
|
55
|
+
- lib/rsql/eval_context.rb
|
56
|
+
- lib/rsql/mysql_results.rb
|
57
|
+
- lib/rsql/mysql.rb
|
58
|
+
homepage: https://github.com/bradrf/rsql
|
59
|
+
licenses: []
|
60
|
+
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options: []
|
63
|
+
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
hash: 51
|
72
|
+
segments:
|
73
|
+
- 1
|
74
|
+
- 8
|
75
|
+
- 2
|
76
|
+
version: 1.8.2
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
hash: 3
|
83
|
+
segments:
|
84
|
+
- 0
|
85
|
+
version: "0"
|
86
|
+
requirements: []
|
87
|
+
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 1.7.2
|
90
|
+
signing_key:
|
91
|
+
specification_version: 3
|
92
|
+
summary: Ruby based MySQL command line with recipes.
|
93
|
+
test_files: []
|
94
|
+
|