rsql 0.2.7 → 0.2.8

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,470 @@
1
+ #--
2
+ # Copyright (C) 2011-2012 by Brad Robel-Forrest <brad+rsql@gigglewax.com>
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+
22
+ # The standard MySQL hooks block Ruby threads, this interface provides an
23
+ # asynchronous query.
24
+ #
25
+ require 'mysqlplus'
26
+
27
+ class Mysql
28
+ alias :query :async_query
29
+ end
30
+
31
+ module RSQL
32
+
33
+ ########################################
34
+ # A wrapper to make it easier to work with MySQL results (and prettier).
35
+ #
36
+ class MySQLResults
37
+
38
+ HEX_RANGE = [
39
+ Mysql::Field::TYPE_BLOB,
40
+ Mysql::Field::TYPE_STRING,
41
+ ]
42
+
43
+ @@conn = nil
44
+ @@field_separator = ' '
45
+ @@max_rows = 1000
46
+ @@database_name = nil
47
+ @@name_cache = {}
48
+ @@history = []
49
+ @@max_history = 10
50
+
51
+ class MaxRowsException < RangeError
52
+ def initialize(rows, max)
53
+ @rows = rows
54
+ @max = max
55
+ end
56
+ attr_reader :rows, :max
57
+ end
58
+
59
+ class << self
60
+
61
+ # Get the underlying MySQL connection object in use.
62
+ #
63
+ def conn; @@conn; end
64
+
65
+ # Set the underlying MySQL connection object to use which
66
+ # implicitly resets the name cache.
67
+ #
68
+ def conn=(conn)
69
+ @@conn = conn
70
+ reset_cache
71
+ end
72
+
73
+ # Get the field separator to use when writing rows in
74
+ # columns.
75
+ #
76
+ def field_separator; @@field_separator; end
77
+
78
+ # Set the field separator to use when writing rows in
79
+ # columns.
80
+ #
81
+ def field_separator=(sep); @@field_separator = sep; end
82
+
83
+ # Get the maximum number of rows to process before
84
+ # throwing a MaxRowsException.
85
+ #
86
+ def max_rows; @@max_rows; end
87
+
88
+ # Set the maximum number of rows to process before
89
+ # throwing a MaxRowsException.
90
+ #
91
+ def max_rows=(cnt); @@max_rows = cnt; end
92
+
93
+ # Get the name of the current database in use.
94
+ #
95
+ def database_name; @@database_name; end
96
+
97
+ # Set the name of the current database in use.
98
+ #
99
+ def database_name=(database); @@database_name = database; end
100
+
101
+ # Get a list of the most recent query strings.
102
+ #
103
+ def history(cnt=:all)
104
+ if Integer === cnt && cnt < @@history.size
105
+ @@history[-cnt,cnt]
106
+ else
107
+ @@history
108
+ end
109
+ end
110
+
111
+ # Reset the history to an empty list.
112
+ #
113
+ def reset_history
114
+ @@history = []
115
+ end
116
+
117
+ # Get the maximum number of historical entries to retain.
118
+ #
119
+ def get_max_history; @@max_history; end
120
+
121
+ # Set the maximum number of historical entries to retain.
122
+ #
123
+ def set_max_history=(count); @@max_history = count; end
124
+
125
+ # Get the list of databases available.
126
+ #
127
+ def databases
128
+ @@name_cache.keys.sort
129
+ end
130
+
131
+ # Get the list of tables available for the current
132
+ # database or a specific one.
133
+ #
134
+ def tables(database=@@database_name)
135
+ @@name_cache[database] || []
136
+ end
137
+
138
+ # Force the database and table names cache to be (re)loaded.
139
+ #
140
+ def reset_cache
141
+ @@name_cache = {}
142
+ begin
143
+ if @@conn
144
+ @@conn.list_dbs.each do |db_name|
145
+ @@conn.select_db(db_name)
146
+ @@name_cache[db_name] = @@conn.list_tables.sort
147
+ end
148
+ end
149
+ rescue Mysql::Error => ex
150
+ ensure
151
+ if @@conn && @@database_name
152
+ @@conn.select_db(@@database_name)
153
+ end
154
+ end
155
+ end
156
+
157
+ # Provide a list of tab completions given the prompted
158
+ # case-insensitive value.
159
+ #
160
+ def complete(str)
161
+ return [] unless @@conn
162
+
163
+ ret = []
164
+
165
+ # offer table names from a specific database
166
+ if str =~ /^([^.]+)\.(.*)$/
167
+ db = $1
168
+ tb = $2
169
+ @@name_cache.each do |db_name, tnames|
170
+ if db.casecmp(db_name) == 0
171
+ tnames.each do |n|
172
+ if m = n.match(/^(#{tb})/i)
173
+ ret << "#{db_name}.#{n}"
174
+ end
175
+ end
176
+ break
177
+ end
178
+ end
179
+ else
180
+ @@name_cache.each do |db_name, tnames|
181
+ if db_name == @@database_name
182
+ tnames.each do |n|
183
+ if m = n.match(/^(#{str})/i)
184
+ ret << n
185
+ end
186
+ end
187
+ elsif m = db_name.match(/^(#{str})/i)
188
+ ret << db_name
189
+ end
190
+ end
191
+ end
192
+
193
+ return ret.sort
194
+ end
195
+
196
+ # Get results from a query.
197
+ #
198
+ def query(sql, eval_context, raw=false, max_rows=@@max_rows)
199
+ @@history.shift if @@max_history <= @@history.size
200
+ @@history << sql
201
+
202
+ start = Time.now.to_f
203
+ results = @@conn.query(sql)
204
+ elapsed = Time.now.to_f - start.to_f
205
+
206
+ affected_rows = @@conn.affected_rows
207
+ unless results && 0 < results.num_rows
208
+ return new(sql, elapsed, affected_rows)
209
+ end
210
+
211
+ if max_rows < results.num_rows
212
+ raise MaxRowsException.new(results.num_rows, max_rows)
213
+ end
214
+
215
+ # extract mysql results into our own table so we can predetermine the
216
+ # lengths of columns and give users a chance to reformat column data
217
+ # before it's displayed (via the bang maps)
218
+
219
+ fields = results.fetch_fields
220
+ fields.collect! do |field|
221
+ def field.longest_length=(len); @longest_length = len; end
222
+ def field.longest_length; @longest_length; end
223
+ field.longest_length = field.name.length
224
+ field
225
+ end
226
+
227
+ results_table = []
228
+ while vals = results.fetch_row
229
+ row = []
230
+ fields.each_with_index do |field, i|
231
+ if raw
232
+ val = vals[i]
233
+ else
234
+ val = eval_context.bang_eval(field.name, vals[i])
235
+ if val.nil?
236
+ val = 'NULL'
237
+ elsif HEX_RANGE.include?(field.type) && val =~ /[^[:print:]\s]/
238
+ val = eval_context.to_hexstr(val)
239
+ end
240
+ end
241
+ vlen = val.respond_to?(:length) ? val.length : 0
242
+ if field.longest_length < vlen
243
+ if String === val
244
+ # consider only the longest line length since some
245
+ # output contains multiple lines like "show create table"
246
+ longest_line = val.split(/\r?\n/).collect{|l|l.length}.max
247
+ if field.longest_length < longest_line
248
+ field.longest_length = longest_line
249
+ end
250
+ else
251
+ field.longest_length = val.length
252
+ end
253
+ end
254
+ row << val
255
+ end
256
+ results_table << row
257
+ end
258
+
259
+ return new(sql, elapsed, affected_rows, fields, results_table)
260
+ end
261
+
262
+ end # class << self
263
+
264
+ ########################################
265
+
266
+ def initialize(sql, elapsed, affected_rows,
267
+ fields=nil, table=nil, field_separator=@@field_separator)
268
+ @sql = sql;
269
+ @elapsed = elapsed;
270
+ @affected_rows = affected_rows;
271
+ @fields = fields
272
+ @table = table
273
+ @field_separator = field_separator
274
+
275
+ # we set this here so that (a) it occurs _after_ we are
276
+ # successful and so we can show an appropriate messge in a
277
+ # displayer
278
+ if @sql.match(/use\s+(\S+)/i)
279
+ @database_changed = true
280
+ @@database_name = $1
281
+ end
282
+ end
283
+
284
+ # Get the query associated with these results.
285
+ #
286
+ attr_reader :sql
287
+
288
+ # Get the number of rows that were affected by the query.
289
+ #
290
+ attr_reader :affected_rows
291
+
292
+ # Get the amount of elapsed time taken by the query.
293
+ #
294
+ attr_reader :elapsed
295
+
296
+ # Determine if there are any results.
297
+ #
298
+ def any?
299
+ !@table.nil?
300
+ end
301
+
302
+ # Determine if there are no results.
303
+ #
304
+ def empty?
305
+ @table.nil?
306
+ end
307
+
308
+ # Get the number of rows available in the results.
309
+ #
310
+ def num_rows
311
+ @table ? @table.size : 0
312
+ end
313
+
314
+ # Get a row from the table hashed with the field names.
315
+ #
316
+ def [](index)
317
+ if !@fields || !@table
318
+ return nil
319
+ end
320
+ if row = @table[index]
321
+ hash = {}
322
+ @fields.each_with_index {|f,i| hash[f.name] = row[i]}
323
+ return hash
324
+ else
325
+ return nil
326
+ end
327
+ end
328
+
329
+ # Iterate through each row of the table hashed with the field names.
330
+ #
331
+ def each_hash(&block)
332
+ if @table
333
+ @table.each do |row|
334
+ hash = {}
335
+ @fields.each_with_index {|f,i| hash[f.name] = row[i]}
336
+ yield(hash)
337
+ end
338
+ end
339
+ end
340
+
341
+ # Remove all rows that do NOT match the expression. Returns true if any
342
+ # matches were found.
343
+ #
344
+ # Options:
345
+ # :fixed => indicates that the string should be escaped of any special characters
346
+ # :nocolor => will not add color escape codes to indicate the match
347
+ # :inverse => reverses the regular expression match
348
+ #
349
+ def grep(pattern, *gopts)
350
+ if @table
351
+ nocolor = gopts.include?(:nocolor)
352
+
353
+ if inverted = gopts.include?(:inverse)
354
+ # there's no point in coloring matches we are removing
355
+ nocolor = true
356
+ end
357
+
358
+ if gopts.include?(:fixed)
359
+ regexp = Regexp.new(/#{Regexp.escape(pattern.to_str)}/)
360
+ elsif Regexp === pattern
361
+ regexp = pattern
362
+ else
363
+ regexp = Regexp.new(/#{pattern.to_str}/)
364
+ end
365
+
366
+ rval = inverted
367
+
368
+ @table.delete_if do |row|
369
+ matched = false
370
+ row.each do |val|
371
+ val = val.to_str unless String === val
372
+ if nocolor
373
+ if matched = !val.match(regexp).nil?
374
+ rval = inverted ? false : true
375
+ break
376
+ end
377
+ else
378
+ # in the color case, we want to colorize all hits in
379
+ # all columns, so we can't early terminate our
380
+ # search
381
+ if val.gsub!(regexp){|m| "\e[31;1m#{m}\e[0m"}
382
+ matched = true
383
+ rval = inverted ? false : true
384
+ end
385
+ end
386
+ end
387
+ inverted ? matched : !matched
388
+ end
389
+
390
+ return rval
391
+ else
392
+ return false
393
+ end
394
+ end
395
+
396
+ # Show a set of results in a decent fashion.
397
+ #
398
+ def display_by_column(io=$stdout)
399
+ if @fields && @table
400
+ fmts = []
401
+ names = []
402
+ len = 0
403
+ @fields.each do |field|
404
+ fmts << "%-#{field.longest_length}s"
405
+ names << field.name
406
+ len += field.longest_length
407
+ end
408
+
409
+ fmt = fmts.join(@field_separator)
410
+ sep = '-' * (len + fmts.length)
411
+ io.puts(fmt % names, sep)
412
+ @table.each{|row| io.puts(fmt % row)}
413
+ display_stats(io, sep)
414
+ else
415
+ display_stats(io)
416
+ end
417
+ end
418
+
419
+ # Show a set of results with a single character separation.
420
+ #
421
+ def display_by_batch(io=$stdout)
422
+ if @fields && @table
423
+ fmt = (['%s'] * @fields.size).join(@field_separator)
424
+ @table.each{|row| io.puts(fmt % row)}
425
+ end
426
+ end
427
+
428
+ # Show a set of results line separated.
429
+ #
430
+ def display_by_line(io=$stdout)
431
+ if @fields && @table
432
+ namelen = 0
433
+ @fields.each do |field|
434
+ namelen = field.name.length if namelen < field.name.length
435
+ end
436
+ namelen += 1
437
+
438
+ @table.each_with_index do |row, i|
439
+ io.puts("#{'*'*30} #{i+1}. row #{'*'*30}")
440
+ row.each_with_index do |val, vi|
441
+ io.printf("%#{namelen}s #{val}#{$/}", @fields[vi].name + ':')
442
+ end
443
+ end
444
+ end
445
+ display_stats(io)
446
+ end
447
+
448
+ # Show a summary line of the results.
449
+ #
450
+ def display_stats(io=$stdout, hdr='')
451
+ if @table
452
+ if @database_changed
453
+ io.puts(hdr, "Database changed");
454
+ hdr = ''
455
+ end
456
+ s = 1 == @table.size ? 'row' : 'rows'
457
+ io.puts(hdr, "#{@table.size} #{s} in set (#{'%0.2f'%@elapsed} sec)")
458
+ else
459
+ if @database_changed
460
+ io.puts(hdr, "Database changed");
461
+ else
462
+ s = 1 == @affected_rows ? 'row' : 'rows'
463
+ io.puts(hdr, "Query OK, #{@affected_rows} #{s} affected (#{'%0.2f'%@elapsed} sec)")
464
+ end
465
+ end
466
+ end
467
+
468
+ end # class MySQLResults
469
+
470
+ end # module RSQL