rsql 0.2.7 → 0.2.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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