rsql 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,8 @@
1
+ module RSQL
2
+ VERSION = '0.1.3'
3
+
4
+ require 'rsql/mysql'
5
+ require 'rsql/mysql_results'
6
+ require 'rsql/eval_context'
7
+ require 'rsql/commands'
8
+ end
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
+