rsql 0.2.6 → 0.2.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,410 +0,0 @@
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
105
- @@history[-cnt,cnt]
106
- else
107
- @@history
108
- end
109
- end
110
-
111
- # Get the maximum number of historical entries to retain.
112
- #
113
- def get_max_history; @@max_history; end
114
-
115
- # Set the maximum number of historical entries to retain.
116
- #
117
- def set_max_history=(count); @@max_history = count; end
118
-
119
- # Get the list of databases available.
120
- #
121
- def databases
122
- @@name_cache.keys.sort
123
- end
124
-
125
- # Get the list of tables available for the current
126
- # database or a specific one.
127
- #
128
- def tables(database=@@database_name)
129
- @@name_cache[database] || []
130
- end
131
-
132
- # Force the database and table names cache to be (re)loaded.
133
- #
134
- def reset_cache
135
- @@name_cache = {}
136
- begin
137
- if @@conn
138
- @@conn.list_dbs.each do |db_name|
139
- @@conn.select_db(db_name)
140
- @@name_cache[db_name] = @@conn.list_tables.sort
141
- end
142
- end
143
- rescue Mysql::Error => ex
144
- ensure
145
- if @@conn && @@database_name
146
- @@conn.select_db(@@database_name)
147
- end
148
- end
149
- end
150
-
151
- # Provide a list of tab completions given the prompted
152
- # case-insensitive value.
153
- #
154
- def complete(str)
155
- return [] unless @@conn
156
-
157
- ret = []
158
-
159
- # offer table names from a specific database
160
- if str =~ /^([^.]+)\.(.*)$/
161
- db = $1
162
- tb = $2
163
- @@name_cache.each do |db_name, tnames|
164
- if db.casecmp(db_name) == 0
165
- tnames.each do |n|
166
- if m = n.match(/^(#{tb})/i)
167
- ret << "#{db_name}.#{n}"
168
- end
169
- end
170
- break
171
- end
172
- end
173
- else
174
- @@name_cache.each do |db_name, tnames|
175
- if db_name == @@database_name
176
- tnames.each do |n|
177
- if m = n.match(/^(#{str})/i)
178
- ret << n
179
- end
180
- end
181
- elsif m = db_name.match(/^(#{str})/i)
182
- ret << db_name
183
- end
184
- end
185
- end
186
-
187
- return ret.sort
188
- end
189
-
190
- # Get results from a query.
191
- #
192
- def query(sql, eval_context, raw=false, max_rows=@@max_rows)
193
- @@history.shift if @@max_history <= @@history.size
194
- @@history << sql
195
-
196
- start = Time.now.to_f
197
- results = @@conn.query(sql)
198
- elapsed = Time.now.to_f - start.to_f
199
-
200
- affected_rows = @@conn.affected_rows
201
- unless results && 0 < results.num_rows
202
- return new(sql, elapsed, affected_rows)
203
- end
204
-
205
- if max_rows < results.num_rows
206
- raise MaxRowsException.new(results.num_rows, max_rows)
207
- end
208
-
209
- # extract mysql results into our own table so we can predetermine the
210
- # lengths of columns and give users a chance to reformat column data
211
- # before it's displayed (via the bang maps)
212
-
213
- fields = results.fetch_fields
214
- fields.collect! do |field|
215
- def field.longest_length=(len); @longest_length = len; end
216
- def field.longest_length; @longest_length; end
217
- field.longest_length = field.name.length
218
- field
219
- end
220
-
221
- results_table = []
222
- while vals = results.fetch_row
223
- row = []
224
- fields.each_with_index do |field, i|
225
- if raw
226
- val = vals[i]
227
- else
228
- val = eval_context.bang_eval(field.name, vals[i])
229
- if val.nil?
230
- val = 'NULL'
231
- elsif HEX_RANGE.include?(field.type) && val =~ /[^[:print:]\s]/
232
- val = eval_context.to_hexstr(val)
233
- end
234
- end
235
- vlen = val.respond_to?(:length) ? val.length : 0
236
- if field.longest_length < vlen
237
- if String === val
238
- # consider only the longest line length since some
239
- # output contains multiple lines like "show create table"
240
- longest_line = val.split(/\r?\n/).collect{|l|l.length}.max
241
- if field.longest_length < longest_line
242
- field.longest_length = longest_line
243
- end
244
- else
245
- field.longest_length = val.length
246
- end
247
- end
248
- row << val
249
- end
250
- results_table << row
251
- end
252
-
253
- return new(sql, elapsed, affected_rows, fields, results_table)
254
- end
255
-
256
- end # class << self
257
-
258
- ########################################
259
-
260
- def initialize(sql, elapsed, affected_rows,
261
- fields=nil, table=nil, field_separator=@@field_separator)
262
- @sql = sql;
263
- @elapsed = elapsed;
264
- @affected_rows = affected_rows;
265
- @fields = fields
266
- @table = table
267
- @field_separator = field_separator
268
-
269
- # we set this here so that (a) it occurs _after_ we are
270
- # successful and so we can show an appropriate messge in a
271
- # displayer
272
- if @sql.match(/use\s+(\S+)/i)
273
- @database_changed = true
274
- @@database_name = $1
275
- end
276
- end
277
-
278
- # Get the query associated with these results.
279
- #
280
- attr_reader :sql
281
-
282
- # Get the number of rows that were affected by the query.
283
- #
284
- attr_reader :affected_rows
285
-
286
- # Get the amount of elapsed time taken by the query.
287
- #
288
- attr_reader :elapsed
289
-
290
- # Determine if there are any results.
291
- #
292
- def any?
293
- !@table.nil?
294
- end
295
-
296
- # Determine if there are no results.
297
- #
298
- def empty?
299
- @table.nil?
300
- end
301
-
302
- # Get the number of rows available in the results.
303
- #
304
- def num_rows
305
- @table ? @table.size : 0
306
- end
307
-
308
- # Get a row from the table hashed with the field names.
309
- #
310
- def [](index)
311
- if !@fields || !@table
312
- return nil
313
- end
314
- if row = @table[index]
315
- hash = {}
316
- @fields.each_with_index {|f,i| hash[f.name] = row[i]}
317
- return hash
318
- else
319
- return nil
320
- end
321
- end
322
-
323
- # Iterate through each row of the table hashed with the field
324
- # names.
325
- #
326
- def each_hash(&block)
327
- if @table
328
- @table.each do |row|
329
- hash = {}
330
- @fields.each_with_index {|f,i| hash[f.name] = row[i]}
331
- yield(hash)
332
- end
333
- end
334
- end
335
-
336
- # Show a set of results in a decent fashion.
337
- #
338
- def display_by_column(io=$stdout)
339
- if @fields && @table
340
- fmts = []
341
- names = []
342
- len = 0
343
- @fields.each do |field|
344
- fmts << "%-#{field.longest_length}s"
345
- names << field.name
346
- len += field.longest_length
347
- end
348
-
349
- fmt = fmts.join(@field_separator)
350
- sep = '-' * (len + fmts.length)
351
- io.puts(fmt % names, sep)
352
- @table.each{|row| io.puts(fmt % row)}
353
- display_stats(io, sep)
354
- else
355
- display_stats(io)
356
- end
357
- end
358
-
359
- # Show a set of results with a single character separation.
360
- #
361
- def display_by_batch(io=$stdout)
362
- if @fields && @table
363
- fmt = (['%s'] * @fields.size).join(@field_separator)
364
- @table.each{|row| io.puts(fmt % row)}
365
- end
366
- end
367
-
368
- # Show a set of results line separated.
369
- #
370
- def display_by_line(io=$stdout)
371
- if @fields && @table
372
- namelen = 0
373
- @fields.each do |field|
374
- namelen = field.name.length if namelen < field.name.length
375
- end
376
- namelen += 1
377
-
378
- @table.each_with_index do |row, i|
379
- io.puts("#{'*'*30} #{i+1}. row #{'*'*30}")
380
- row.each_with_index do |val, vi|
381
- io.printf("%#{namelen}s #{val}#{$/}", @fields[vi].name + ':')
382
- end
383
- end
384
- end
385
- display_stats(io)
386
- end
387
-
388
- # Show a summary line of the results.
389
- #
390
- def display_stats(io=$stdout, hdr='')
391
- if @table
392
- if @database_changed
393
- io.puts(hdr, "Database changed");
394
- hdr = ''
395
- end
396
- s = 1 == @table.size ? 'row' : 'rows'
397
- io.puts(hdr, "#{@table.size} #{s} in set (#{'%0.2f'%@elapsed} sec)")
398
- else
399
- if @database_changed
400
- io.puts(hdr, "Database changed");
401
- else
402
- s = 1 == @affected_rows ? 'row' : 'rows'
403
- io.puts(hdr, "Query OK, #{@affected_rows} #{s} affected (#{'%0.2f'%@elapsed} sec)")
404
- end
405
- end
406
- end
407
-
408
- end # class MySQLResults
409
-
410
- end # module RSQL
@@ -1,85 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'test/unit'
4
-
5
- begin
6
- require 'rubygems'
7
- rescue LoadError
8
- end
9
- require 'mocha'
10
-
11
- $: << File.expand_path(File.join(File.dirname(__FILE__),'..','lib')) << File.dirname(__FILE__)
12
- require 'rsql/mysql_results.rb'
13
- require 'rsql/eval_context.rb'
14
- require 'rsql/commands.rb'
15
-
16
- class TestCommands < Test::Unit::TestCase
17
-
18
- include RSQL
19
-
20
- def setup
21
- @orig_stdout = $stdout
22
- $stdout = @strout = StringIO.new
23
- @ctx = EvalContext.new
24
- @conn = mock('Mysql')
25
- @conn.expects(:list_dbs).returns([])
26
- MySQLResults.conn = @conn
27
- end
28
-
29
- def teardown
30
- $stdout = @orig_stdout
31
- end
32
-
33
- def test_simple_ruby
34
- cmds = Commands.new('. puts :hello', :display_by_column)
35
- assert_equal(false, cmds.empty?)
36
- assert_not_nil(cmds.last)
37
- cmds.run!(@ctx)
38
- assert_equal('hello', @strout.string.chomp)
39
- end
40
-
41
- def test_simple_sql
42
- cmds = Commands.new('do some silly stuff', :display_by_column)
43
- @conn.expects(:query).with(instance_of(String)).returns(nil)
44
- @conn.expects(:affected_rows).returns(1)
45
- cmds.run!(@ctx)
46
- assert_match(/Query OK, 1 row affected/, @strout.string)
47
- end
48
-
49
- def test_separators
50
- cmds = Commands.new('. puts :hello\; puts :world;', :display_by_column)
51
- cmds.run!(@ctx)
52
- assert_equal('hello'+$/+'world', @strout.string.chomp)
53
-
54
- # make sure our logic to handle eval'd blocks with args works
55
- @strout.string = ''
56
- cmds = Commands.new('. Proc.new{|a| puts a.inspect} | @results.value.call(:fancy)', :display_by_column)
57
- cmds.run!(@ctx)
58
- assert_equal(':fancy', @strout.string.chomp)
59
- end
60
-
61
- def test_multiple
62
- @conn.expects(:query).with('one thing').returns(nil)
63
- @conn.expects(:affected_rows).returns(1)
64
- cmds = Commands.new('. "one thing" ; . puts :hello.inspect', :display_by_column)
65
- cmds.run!(@ctx)
66
- assert_match(/^QueryOK,1rowaffected\(\d+.\d+sec\):hello$/,
67
- @strout.string.gsub(/\s+/,''))
68
- end
69
-
70
- def test_bangs
71
- cmds = Commands.new('silly stuff ! this => that', :display_by_column)
72
- @conn.expects(:query).with('silly stuff').returns(nil)
73
- @conn.expects(:affected_rows).returns(13)
74
- cmds.run!(@ctx)
75
- assert_match(/Query OK, 13 rows affected/, @strout.string)
76
-
77
- # now test logic to continue if it _doesn't_ look like a bang
78
- cmds = Commands.new('silly stuff ! more things', :display_by_column)
79
- @conn.expects(:query).with('silly stuff ! more things').returns(nil)
80
- @conn.expects(:affected_rows).returns(4)
81
- cmds.run!(@ctx)
82
- assert_match(/Query OK, 4 rows affected/, @strout.string)
83
- end
84
-
85
- end # class TestCommands