rsql 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,219 @@
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
+ require 'stringio'
24
+
25
+ EvalResults = Struct.new(:value, :stdout)
26
+
27
+ ########################################
28
+ # A wrapper to parse and handle commands
29
+ #
30
+ class Commands
31
+
32
+ Command = Struct.new(:content, :bangs, :declarator, :displayer)
33
+
34
+ ########################################
35
+
36
+ # split on separators, allowing for escaping
37
+ #
38
+ SEPARATORS = ';|!'
39
+ def initialize(input, default_displayer)
40
+ @default_displayer = default_displayer
41
+ @cmds = []
42
+ esc = ''
43
+ bangs = {}
44
+ match_before_bang = nil
45
+ in_pipe_arg = false
46
+ next_is_ruby = false
47
+
48
+ input.scan(/[^#{SEPARATORS}]+.?/) do |match|
49
+ if i = SEPARATORS.index(match[-1])
50
+ sep = SEPARATORS[i]
51
+ match.chop!
52
+
53
+ if match[-1] == ?\\
54
+ # unescape the separator and save the content away
55
+ esc << match[0..-2] << sep
56
+ next
57
+ end
58
+ else
59
+ sep = nil
60
+ end
61
+
62
+ if esc.any?
63
+ esc << match
64
+ match = esc
65
+ esc = ''
66
+ end
67
+
68
+ if match_before_bang
69
+ new_bangs = {}
70
+ match.split(/\s*,\s*/).each do |ent|
71
+ (key,val) = ent.split(/\s*=>\s*/)
72
+ unless key && val
73
+ # they are using a bang but have no maps
74
+ # so we assume this is a != or something
75
+ # similar and let it go through unmapped
76
+ esc = match_before_bang + '!' + match
77
+ match_before_bang = nil
78
+ break
79
+ end
80
+ new_bangs[key.strip] = val.to_sym
81
+ end
82
+ next unless match_before_bang
83
+ match = match_before_bang
84
+ match_before_bang = nil
85
+ bangs.merge!(new_bangs)
86
+ end
87
+
88
+ if sep == ?!
89
+ match_before_bang = match
90
+ next
91
+ end
92
+
93
+ if sep == ?|
94
+ # we've split on a pipe so we need to handle the
95
+ # case where ruby code is declaring a block with
96
+ # arguments (e.g. {|x| p x} or do |x| p x end)
97
+ if in_pipe_arg
98
+ in_pipe_arg = false
99
+ esc << match << '|'
100
+ next
101
+ elsif match =~ /\{\s*|do\s*/
102
+ in_pipe_arg = true
103
+ esc << match << '|'
104
+ next
105
+ end
106
+ end
107
+
108
+ add_command(match, bangs, next_is_ruby, sep)
109
+
110
+ bangs = {}
111
+ next_is_ruby = sep == ?|
112
+ end
113
+
114
+ add_command(esc, bangs, next_is_ruby)
115
+ end
116
+
117
+ def empty?
118
+ return @cmds.empty?
119
+ end
120
+
121
+ def run!(eval_context)
122
+ last_results = nil
123
+ while @cmds.any?
124
+ cmd = @cmds.shift
125
+ results = run_command(cmd, last_results, eval_context)
126
+ return :done if results == :done
127
+
128
+ if cmd.displayer == :pipe
129
+ last_results = results
130
+ elsif MySQLResults === results
131
+ last_results = nil
132
+ results.send(cmd.displayer)
133
+ elsif EvalResults === results
134
+ last_results = nil
135
+ if results.stdout && 0 < results.stdout.size
136
+ puts results.stdout.string
137
+ end
138
+ puts "=> #{results.value.inspect}" if results.value
139
+ end
140
+ end
141
+ end
142
+
143
+ ########################################
144
+ private
145
+
146
+ def add_command(content, bangs, is_ruby, separator=nil)
147
+ content.strip!
148
+
149
+ case content[0]
150
+ when ?.
151
+ content.slice!(0)
152
+ declarator = :ruby
153
+ when ?@
154
+ content.slice!(0)
155
+ declarator = :iterator
156
+ else
157
+ declarator = is_ruby ? :ruby : nil
158
+ end
159
+
160
+ if content.end_with?('\G')
161
+ # emulate mysql's \G output
162
+ content.slice!(-2,2)
163
+ displayer = :display_by_line
164
+ elsif separator == ?|
165
+ displayer = :pipe
166
+ else
167
+ displayer = @default_displayer
168
+ end
169
+
170
+ if content.any?
171
+ @cmds << Command.new(content, bangs, declarator, displayer)
172
+ return true
173
+ end
174
+
175
+ return false
176
+ end
177
+
178
+ def run_command(cmd, last_results, eval_context)
179
+ ctx = EvalContext::CommandContext.new
180
+
181
+ # set up to allow an iterator to run up to 100,000 times
182
+ 100000.times do |i|
183
+ eval_context.bangs = cmd.bangs
184
+
185
+ if cmd.declarator
186
+ ctx.index = i
187
+ ctx.last_results = last_results
188
+ stdout = cmd.displayer == :pipe ? StringIO.new : nil
189
+ value = eval_context.safe_eval(cmd.content, ctx, stdout)
190
+ else
191
+ value = cmd.content
192
+ end
193
+
194
+ return :done if value == 'exit' || value == 'quit'
195
+
196
+ if String === value
197
+ begin
198
+ last_results = MySQLResults.query(value, eval_context)
199
+ rescue MySQLResults::MaxRowsException => ex
200
+ $stderr.puts "refusing to process #{ex.rows} rows (max: #{ex.max})"
201
+ rescue MysqlError => ex
202
+ $stderr.puts ex.message
203
+ rescue Exception => ex
204
+ $stderr.puts ex.inspect
205
+ raise
206
+ end
207
+ else
208
+ last_results = EvalResults.new(value, stdout)
209
+ end
210
+
211
+ break unless ctx.incomplete
212
+ end
213
+
214
+ return last_results
215
+ end
216
+
217
+ end # class Commands
218
+
219
+ end # module RSQL
@@ -0,0 +1,414 @@
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
+ # This class wraps all dynamic evaluation and serves as the reflection
25
+ # class for adding new methods dynamically.
26
+ #
27
+ class EvalContext
28
+
29
+ Registration = Struct.new(:name, :args, :bangs, :block, :usage, :desc)
30
+
31
+ CommandContext = Struct.new(:index, :incomplete, :last_results, :state)
32
+
33
+ HEXSTR_LIMIT = 32
34
+
35
+ def initialize
36
+ @hexstr_limit = HEXSTR_LIMIT
37
+ @last_cmd = nil
38
+
39
+ @loaded_fns = []
40
+ @init_registrations = []
41
+ @bangs = {}
42
+
43
+ @registrations = {
44
+ :version => Registration.new('version', [], {},
45
+ method(:version),
46
+ 'version',
47
+ 'RSQL version information.'),
48
+ :reload => Registration.new('reload', [], {},
49
+ method(:reload),
50
+ 'reload',
51
+ 'Reload the rsqlrc file.'),
52
+ :last_cmd => Registration.new('last_cmd', [], {},
53
+ Proc.new{puts @last_cmd},
54
+ 'last_cmd',
55
+ 'Print the last command generated.'),
56
+ :set_max_rows => Registration.new('set_max_rows', [], {},
57
+ Proc.new{|r| MySQLResults.max_rows = r},
58
+ 'set_max_rows',
59
+ 'Set the maximum number of rows to process.'),
60
+ :max_rows => Registration.new('max_rows', [], {},
61
+ Proc.new{MySQLResults.max_rows},
62
+ 'max_rows',
63
+ 'Get the maximum number of rows to process.'),
64
+ }
65
+ end
66
+
67
+ attr_accessor :bangs
68
+
69
+ def call_init_registrations(mysql)
70
+ @init_registrations.each do |sym|
71
+ reg = @registrations[sym]
72
+ sql = reg.block.call(*reg.args)
73
+ mysql.query(sql) if String === sql
74
+ end
75
+ end
76
+
77
+ def load(fn)
78
+ ret = Thread.new {
79
+ begin
80
+ eval(File.read(fn), binding, fn)
81
+ nil
82
+ rescue Exception => ex
83
+ ex
84
+ end
85
+ }.value
86
+
87
+ if Exception === ret
88
+ bt = ret.backtrace.collect{|line| line.start_with?(fn) ? line : nil}.compact
89
+ $stderr.puts("#{ret.class}: #{ret.message}", bt, '')
90
+ else
91
+ @loaded_fns << fn unless @loaded_fns.include?(fn)
92
+ end
93
+ end
94
+
95
+ def reload
96
+ @loaded_fns.each{|fn| self.load(fn)}
97
+ puts "loaded: #{@loaded_fns.inspect}"
98
+ end
99
+
100
+ def bang_eval(field, val)
101
+ if bang = @bangs[field]
102
+ begin
103
+ val = Thread.new{ eval("$SAFE=2;#{bang}(val)") }.value
104
+ rescue Exception => ex
105
+ $stderr.puts(ex.message, ex.backtrace.first)
106
+ end
107
+ end
108
+
109
+ return val
110
+ end
111
+
112
+ # safely evaluate Ruby content within our context
113
+ #
114
+ def safe_eval(content, context, stdout)
115
+ @command_context = context
116
+
117
+ # allow a simple reload to be called directly as it requires a
118
+ # little looser safety valve...
119
+ if 'reload' == content
120
+ reload
121
+ return
122
+ end
123
+
124
+ # same relaxed call to load too
125
+ if m = /^\s*load\s+'(.+)'\s*$/.match(content)
126
+ self.load(m[1])
127
+ return
128
+ end
129
+
130
+ if stdout
131
+ # capture stdout
132
+ orig_stdout = $stdout
133
+ $stdout = stdout
134
+ end
135
+
136
+ begin
137
+ value = Thread.new{ eval('$SAFE=2;' + content) }.value
138
+ rescue Exception => ex
139
+ $stderr.puts(ex.message.gsub(/\(eval\):\d+:/,''))
140
+ ensure
141
+ $stdout = orig_stdout if stdout
142
+ end
143
+
144
+ @last_cmd = value if String === value
145
+
146
+ return value
147
+ end
148
+
149
+ # provide a list of tab completions given the prompted value
150
+ #
151
+ def complete(str)
152
+ if str[0] == ?.
153
+ str.slice!(0)
154
+ prefix = '.'
155
+ else
156
+ prefix = ''
157
+ end
158
+
159
+ ret = MySQLResults.complete(str)
160
+ ret += @registrations.keys.sort_by{|sym|sym.to_s}.collect do |sym|
161
+ name = sym.to_s
162
+ if name.start_with?(str)
163
+ prefix + name
164
+ else
165
+ nil
166
+ end
167
+ end
168
+
169
+ ret.compact!
170
+ ret
171
+ end
172
+
173
+ # reset the hexstr limit back to the default value
174
+ #
175
+ def reset_hexstr_limit
176
+ @hexstr_limit = HEXSTR_LIMIT
177
+ end
178
+
179
+ # convert a binary string value into a hexadecimal string
180
+ #
181
+ def to_hexstr(bin, limit=@hexstr_limit, prefix='0x')
182
+ cnt = 0
183
+ str = prefix << bin.gsub(/./m) do |ch|
184
+ if limit
185
+ if limit < 1
186
+ cnt += 1
187
+ next
188
+ end
189
+ limit -= 1
190
+ end
191
+ '%02x' % ch[0]
192
+ end
193
+
194
+ if limit && limit < 1 && 0 < cnt
195
+ str << "... (#{cnt} bytes hidden)"
196
+ end
197
+
198
+ return str
199
+ end
200
+
201
+ ########################################
202
+ private
203
+
204
+ # display a listing of all registered helpers
205
+ #
206
+ def list
207
+ usagelen = 0
208
+ desclen = 0
209
+
210
+ sorted = @registrations.values.sort_by do |reg|
211
+ usagelen = reg.usage.length if usagelen < reg.usage.length
212
+ longest_line = reg.desc.split(/\r?\n/).collect{|l|l.length}.max
213
+ desclen = longest_line if longest_line && desclen < longest_line
214
+ reg.usage
215
+ end
216
+
217
+ fmt = "%-#{usagelen}s %s#{$/}"
218
+
219
+ printf(fmt, 'usage', 'description')
220
+ puts '-'*(usagelen+2+desclen)
221
+
222
+ sorted.each do |reg|
223
+ printf(fmt, reg.usage, reg.desc)
224
+ end
225
+
226
+ return nil
227
+ end
228
+
229
+ # show all the pertinent version data we have about our
230
+ # software and the mysql connection
231
+ #
232
+ def version
233
+ puts "rsql:v#{RSQL::VERSION} client:v#{MySQLResults.conn.client_info} " \
234
+ "server:v#{MySQLResults.conn.server_info}"
235
+ end
236
+
237
+ # provide a helper utility in the event a registered
238
+ # method would like to make its own queries
239
+ #
240
+ def query(content, *args)
241
+ MySQLResults.query(content, self, *args)
242
+ end
243
+
244
+ # exactly like register below except in addition to registering as
245
+ # a usable call for later, we will also use these as soon as we
246
+ # have a connection to MySQL.
247
+ #
248
+ def register_init(sym, *args, &block)
249
+ register(sym, *args, &block)
250
+ @init_registrations << sym
251
+ end
252
+
253
+ # if given a block, allow the block to be called later, otherwise,
254
+ # create a method whose sole purpose is to dynmaically generate
255
+ # sql with variable interpolation
256
+ #
257
+ def register(sym, *args, &block)
258
+ name = usage = sym.to_s
259
+
260
+ if Hash === args.last
261
+ bangs = args.pop
262
+ desc = bangs.delete(:desc)
263
+ else
264
+ bangs = {}
265
+ end
266
+
267
+ desc = '' unless desc
268
+
269
+ if block.nil?
270
+ sql = sqeeze!(args.pop)
271
+
272
+ argstr = args.join(',')
273
+ usage << "(#{argstr})" unless argstr.empty?
274
+
275
+ blockstr = %{$SAFE=2;lambda{|#{argstr}|%{#{sql}} % [#{argstr}]}}
276
+ block = Thread.new{ eval(blockstr) }.value
277
+ args = []
278
+ else
279
+ usage << "(#{block.arity})" unless 0 == block.arity
280
+ end
281
+
282
+ @registrations[sym] = Registration.new(name, args, bangs, block, usage, desc)
283
+ end
284
+
285
+ # convert a collection of values into to hexadecimal strings
286
+ #
287
+ def hexify(*ids)
288
+ ids.collect do |id|
289
+ case id
290
+ when String
291
+ if id.start_with?('0x')
292
+ id
293
+ else
294
+ '0x' << id
295
+ end
296
+ when Integer
297
+ '0x' << id.to_s(16)
298
+ else
299
+ raise "invalid id: #{id.class}"
300
+ end
301
+ end.join(',')
302
+ end
303
+
304
+ # convert a number of bytes into a human readable string
305
+ #
306
+ def humanize_bytes(bytes)
307
+ abbrev = ['B','KB','MB','GB','TB','PB','EB','ZB','YB']
308
+ bytes = bytes.to_i
309
+ fmt = '%7.2f'
310
+
311
+ abbrev.each_with_index do |a,i|
312
+ if bytes < (1024**(i+1))
313
+ if i == 0
314
+ return "#{fmt % bytes} B"
315
+ else
316
+ b = bytes / (1024.0**i)
317
+ return "#{fmt % b} #{a}"
318
+ end
319
+ end
320
+ end
321
+
322
+ return bytes.to_s
323
+ end
324
+
325
+ # convert a human readable string of bytes into an integer
326
+ #
327
+ def dehumanize_bytes(str)
328
+ abbrev = ['B','KB','MB','GB','TB','PB','EB','ZB','YB']
329
+
330
+ if str =~ /(\d+(\.\d+)?)\s*(\w+)?/
331
+ b = $1.to_f
332
+ if $3
333
+ i = abbrev.index($3.upcase)
334
+ return (b * (1024**i)).round
335
+ else
336
+ return b.round
337
+ end
338
+ end
339
+
340
+ raise "unable to parse '#{str}'"
341
+ end
342
+
343
+ # convert a time into a relative string from now
344
+ #
345
+ def relative_time(dt)
346
+ return dt unless String === dt
347
+
348
+ now = Time.now.utc
349
+ theirs = Time.parse(dt + ' UTC')
350
+ if theirs < now
351
+ diff = now - theirs
352
+ postfix = 'ago'
353
+ else
354
+ diff = theirs - now
355
+ postfix = 'from now'
356
+ end
357
+
358
+ fmt = '%3.0f'
359
+
360
+ [
361
+ [31556926.0, 'years'],
362
+ [2629743.83, 'months'],
363
+ [86400.0, 'days'],
364
+ [3600.0, 'hours'],
365
+ [60.0, 'minutes']
366
+ ].each do |(limit, label)|
367
+ if (limit * 1.5) < diff
368
+ return "#{fmt % (diff / limit)} #{label} #{postfix}"
369
+ end
370
+ end
371
+
372
+ return "#{fmt % diff} seconds #{postfix}"
373
+ end
374
+
375
+ # squeeze out any spaces
376
+ #
377
+ def sqeeze!(sql)
378
+ sql.gsub!(/\s+/,' ')
379
+ sql.strip!
380
+ sql << ';' unless sql[-1] == ?;
381
+ sql
382
+ end
383
+
384
+ # safely store an object into a file keeping at most one
385
+ # backup if the file already exists
386
+ #
387
+ def safe_save(obj, name)
388
+ name += '.yml' unless File.extname(name) == '.yml'
389
+ tn = "#{name}.tmp"
390
+ File.open(tn, 'w'){|f| YAML.dump(obj, f)}
391
+ if File.exist?(name)
392
+ bn = "#{name}~"
393
+ File.unlink(bn) if File.exist?(bn)
394
+ File.rename(name, bn)
395
+ end
396
+ File.rename(tn, name)
397
+ puts "Saved: #{name}"
398
+ end
399
+
400
+ def method_missing(sym, *args, &block)
401
+ if reg = @registrations[sym]
402
+ @bangs.merge!(reg.bangs)
403
+ final_args = reg.args + args
404
+ reg.block.call(*final_args)
405
+ elsif MySQLResults.respond_to?(sym)
406
+ MySQLResults.send(sym, *args)
407
+ else
408
+ super.method_missing(sym, *args, &block)
409
+ end
410
+ end
411
+
412
+ end # class EvalContext
413
+
414
+ end # module RSQL