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