rsql 0.2.6 → 0.2.7

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