rsql 0.1.3

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,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