rsql 0.2.7 → 0.2.8

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,737 @@
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
+ module RSQL
23
+
24
+ require 'ostruct'
25
+ require 'time'
26
+
27
+ # todo: add a simple way to interpolate directly within a sql query about to
28
+ # be exec'd (so we can save stuff from other queries in variables that can
29
+ # then be ref'd in a new query all on the cmd line)
30
+
31
+ ################################################################################
32
+ # This class wraps all dynamic evaluation and serves as the reflection class
33
+ # for adding methods dynamically.
34
+ #
35
+ class EvalContext
36
+
37
+ Registration = Struct.new(:name, :args, :bangs, :block, :usage,
38
+ :desc, :source, :source_fn)
39
+
40
+ HEXSTR_LIMIT = 32
41
+
42
+ def initialize(options=OpenStruct.new)
43
+ @opts = options
44
+ @prompt = nil
45
+ @verbose = @opts.verbose
46
+ @hexstr_limit = HEXSTR_LIMIT
47
+ @results = nil
48
+
49
+ @loaded_fns = []
50
+ @loaded_fns_state = {}
51
+ @init_registrations = []
52
+ @bangs = {}
53
+ @global_bangs = {}
54
+
55
+ @registrations = {
56
+ :version => Registration.new('version', [], {},
57
+ method(:version),
58
+ 'version',
59
+ 'Version information about RSQL, the client, and the server.'),
60
+ :help => Registration.new('help', [], {},
61
+ method(:help),
62
+ 'help',
63
+ 'Show short syntax help.'),
64
+ :grep => Registration.new('grep', [], {},
65
+ method(:grep),
66
+ 'grep',
67
+ 'Show results when regular expression matches any part of the content.'),
68
+ :reload => Registration.new('reload', [], {},
69
+ method(:reload),
70
+ 'reload',
71
+ 'Reload the rsqlrc file.'),
72
+ :desc => Registration.new('desc', [], {},
73
+ method(:desc),
74
+ 'desc',
75
+ 'Describe the content of a recipe.'),
76
+ :history => Registration.new('history', [], {},
77
+ method(:history),
78
+ 'history(cnt=1)',
79
+ 'Print recent queries made (request a count or use :all for entire list).'),
80
+ :set_max_rows => Registration.new('set_max_rows', [], {},
81
+ Proc.new{|r| MySQLResults.max_rows = r},
82
+ 'set_max_rows',
83
+ 'Set the maximum number of rows to process.'),
84
+ :max_rows => Registration.new('max_rows', [], {},
85
+ Proc.new{MySQLResults.max_rows},
86
+ 'max_rows',
87
+ 'Get the maximum number of rows to process.'),
88
+ }
89
+ end
90
+
91
+ attr_reader :prompt
92
+ attr_accessor :bangs, :verbose
93
+
94
+ def call_init_registrations
95
+ @init_registrations.each do |sym|
96
+ reg = @registrations[sym]
97
+ sql = reg.block.call(*reg.args)
98
+ query(sql) if String === sql
99
+ end
100
+ end
101
+
102
+ def load(fn, opt=nil)
103
+ @loaded_fns << fn unless @loaded_fns_state.key?(fn)
104
+ @loaded_fns_state[fn] = :loading
105
+
106
+ # this should only be done after we have established a
107
+ # mysql connection, so this option allows rsql to load the
108
+ # init file immediately and then later make the init
109
+ # registration calls--we set this as an instance variable
110
+ # to allow for loaded files to call load again and yet
111
+ # still maintain the skip logic
112
+ if opt == :skip_init_registrations
113
+ reset_skipping = @skipping_init_registrations = true
114
+ end
115
+
116
+ ret = Thread.new {
117
+ begin
118
+ eval(File.read(fn), binding, fn)
119
+ nil
120
+ rescue Exception => ex
121
+ ex
122
+ end
123
+ }.value
124
+
125
+ if Exception === ret
126
+ @loaded_fns_state[fn] = :failed
127
+ if @verbose
128
+ $stderr.puts("#{ex.class}: #{ex.message}", ex.backtrace)
129
+ else
130
+ bt = ret.backtrace.collect{|line| line.start_with?(fn) ? line : nil}.compact
131
+ $stderr.puts("#{ret.class}: #{ret.message}", bt, '')
132
+ end
133
+ ret = false
134
+ else
135
+ @loaded_fns_state[fn] = :loaded
136
+ call_init_registrations unless @skipping_init_registrations
137
+ ret = true
138
+ end
139
+
140
+ @skipping_init_registrations = false if reset_skipping
141
+
142
+ return ret
143
+ end
144
+
145
+ def reload
146
+ # some files may be loaded by other files, if so, we don't want to
147
+ # reload them again here
148
+ @loaded_fns.each{|fn| @loaded_fns_state[fn] = nil}
149
+ @loaded_fns.each{|fn| self.load(fn, :skip_init_registrations) if @loaded_fns_state[fn] == nil}
150
+
151
+ # load up the inits after all the normal registrations are ready
152
+ call_init_registrations
153
+
154
+ # report all the successfully loaded ones
155
+ loaded = []
156
+ @loaded_fns.each{|fn,state| loaded << fn if @loaded_fns_state[fn] == :loaded}
157
+ puts "loaded: #{loaded.inspect}"
158
+ end
159
+
160
+ def bang_eval(field, val)
161
+ # allow individual bangs to override global ones, even if they're nil
162
+ if @bangs.key?(field)
163
+ bang = @bangs[field]
164
+ else
165
+ # todo: this will run on *every* value--this should be optimized
166
+ # so that it's only run once on each query's result column
167
+ # fields and then we'd know if any bangs are usable and pased in
168
+ # for each result value
169
+ @global_bangs.each do |m,b|
170
+ if (String === m && m == field.to_s) ||
171
+ (Regexp === m && m.match(field.to_s))
172
+ bang = b
173
+ break
174
+ end
175
+ end
176
+ end
177
+
178
+ if bang
179
+ begin
180
+ val = Thread.new{ eval("#{bang}(val)") }.value
181
+ rescue Exception => ex
182
+ if @verbose
183
+ $stderr.puts("#{ex.class}: #{ex.message}", ex.backtrace)
184
+ else
185
+ $stderr.puts(ex.message, ex.backtrace.first)
186
+ end
187
+ end
188
+ end
189
+
190
+ return val
191
+ end
192
+
193
+ # Safely evaluate Ruby content within our context.
194
+ #
195
+ def safe_eval(content, results, stdout)
196
+ @results = results
197
+
198
+ # allow a simple reload to be called directly as it requires a
199
+ # little looser safety valve...
200
+ if 'reload' == content
201
+ reload
202
+ return
203
+ end
204
+
205
+ # same relaxed call to load too
206
+ if m = content.match(/^\s*load\s+'(.+)'\s*$/)
207
+ self.load(m[1])
208
+ return
209
+ end
210
+
211
+ # help out the poor user and fix up any describes
212
+ # requested so they don't need to remember that it needs
213
+ # to be a symbol passed in
214
+ if m = content.match(/^\s*desc\s+([^:]\S+)\s*$/)
215
+ content = "desc :#{m[1]}"
216
+ end
217
+
218
+ if stdout
219
+ # capture stdout
220
+ orig_stdout = $stdout
221
+ $stdout = stdout
222
+ end
223
+
224
+ begin
225
+ # in order to print out errors in a loaded script so
226
+ # that we have file/line info, we need to rescue their
227
+ # exceptions inside the evaluation
228
+ th = Thread.new do
229
+ eval('begin;' << content << %q{
230
+ rescue Exception => ex
231
+ if @verbose
232
+ $stderr.puts("#{ex.class}: #{ex.message}", ex.backtrace)
233
+ else
234
+ bt = []
235
+ ex.backtrace.each do |t|
236
+ break if t.include?('bin/rsql')
237
+ bt << t unless t.include?('lib/rsql/') || t.include?('(eval)')
238
+ end
239
+ $stderr.puts(ex.message.gsub(/\(eval\):\d+:/,''),bt)
240
+ end
241
+ end
242
+ })
243
+ end
244
+ value = th.value
245
+ rescue Exception => ex
246
+ $stderr.puts(ex.message.gsub(/\(eval\):\d+:/,''))
247
+ ensure
248
+ $stdout = orig_stdout if stdout
249
+ end
250
+
251
+ return value
252
+ end
253
+
254
+ # Provide a list of tab completions given the prompted value.
255
+ #
256
+ def complete(str)
257
+ if str[0] == ?.
258
+ str.slice!(0)
259
+ prefix = '.'
260
+ else
261
+ prefix = ''
262
+ end
263
+
264
+ ret = MySQLResults.complete(str)
265
+
266
+ ret += @registrations.keys.sort_by{|sym|sym.to_s}.collect do |sym|
267
+ name = sym.to_s
268
+ if name.start_with?(str)
269
+ prefix + name
270
+ else
271
+ nil
272
+ end
273
+ end
274
+
275
+ ret.compact!
276
+ ret
277
+ end
278
+
279
+ # Reset the hexstr limit back to the default value.
280
+ #
281
+ def reset_hexstr_limit
282
+ @hexstr_limit = HEXSTR_LIMIT
283
+ end
284
+
285
+ # Convert a binary string value into a hexadecimal string.
286
+ #
287
+ def to_hexstr(bin, limit=@hexstr_limit, prefix='0x')
288
+ return bin if bin.nil?
289
+
290
+ cnt = 0
291
+ str = prefix << bin.gsub(/./m) do |ch|
292
+ if limit
293
+ if limit < 1
294
+ cnt += 1
295
+ next
296
+ end
297
+ limit -= 1
298
+ end
299
+ '%02x' % ch.bytes.first
300
+ end
301
+
302
+ if limit && limit < 1 && 0 < cnt
303
+ str << "... (#{cnt} bytes hidden)"
304
+ end
305
+
306
+ return str
307
+ end
308
+
309
+ ########################################
310
+ private
311
+
312
+ # Display a listing of all registered helpers.
313
+ #
314
+ def list # :doc:
315
+ usagelen = 0
316
+ desclen = 0
317
+
318
+ sorted = @registrations.values.sort_by do |reg|
319
+ usagelen = reg.usage.length if usagelen < reg.usage.length
320
+ longest_line = reg.desc.split(/\r?\n/).collect{|l|l.length}.max
321
+ desclen = longest_line if longest_line && desclen < longest_line
322
+ reg.usage
323
+ end
324
+
325
+ fmt = "%-#{usagelen}s %s#{$/}"
326
+
327
+ printf(fmt, 'usage', 'description')
328
+ puts '-'*(usagelen+2+desclen)
329
+
330
+ sorted.each do |reg|
331
+ printf(fmt, reg.usage, reg.desc)
332
+ end
333
+
334
+ return nil
335
+ end
336
+
337
+ # Attempt to locate the parameters of a given block by
338
+ # searching its source.
339
+ #
340
+ def params(name, block)
341
+ params = ''
342
+
343
+ if block.arity != 0 && block.arity != -1 &&
344
+ block.inspect.match(/@(.+):(\d+)>$/)
345
+ fn = $1
346
+ lineno = $2.to_i
347
+
348
+ if fn == '(eval)'
349
+ $stderr.puts "refusing to search an eval block for :#{name}"
350
+ return params
351
+ end
352
+
353
+ File.open(fn) do |f|
354
+ i = 0
355
+ found = false
356
+ while line = f.gets
357
+ i += 1
358
+ next if i < lineno
359
+
360
+ unless found
361
+ # give up if no start found within 20
362
+ # lines
363
+ break if lineno + 20 < i
364
+ if m = line.match(/(\{|do\b)(.*)$/)
365
+ # adjust line to be the remainder
366
+ # after the start
367
+ line = m[2]
368
+ found = true
369
+ else
370
+ next
371
+ end
372
+ end
373
+
374
+ if m = line.match(/^\s*\|([^\|]*)\|/)
375
+ params = "(#{m[1]})"
376
+ break
377
+ end
378
+
379
+ # if the params aren't here then we'd
380
+ # better only have whitespace otherwise
381
+ # this block doesn't have params...even
382
+ # though arity says it should
383
+ next if line.match(/^\s*$/)
384
+ $stderr.puts "unable to locate params for :#{name}"
385
+ break
386
+ end
387
+ end
388
+ end
389
+
390
+ return params
391
+ end
392
+
393
+ # Similiar to the MySQL "desc" command, show the content
394
+ # of nearly any registered recipe including where it was
395
+ # sourced (e.g. what file:line it came from).
396
+ #
397
+ def desc(sym)
398
+ unless Symbol === sym
399
+ $stderr.puts("must provide a Symbol--try prefixing it with a colon (:)")
400
+ return
401
+ end
402
+
403
+ unless reg = @registrations[sym]
404
+ $stderr.puts "nothing registered as #{sym}"
405
+ return
406
+ end
407
+
408
+ if Method === reg.block
409
+ $stderr.puts "refusing to describe the #{sym} method"
410
+ return
411
+ end
412
+
413
+ if !reg.source && reg.block.inspect.match(/@(.+):(\d+)>$/)
414
+ fn = $1
415
+ lineno = $2.to_i
416
+
417
+ if fn == __FILE__
418
+ $stderr.puts "refusing to describe EvalContext##{sym}"
419
+ return
420
+ end
421
+
422
+ if fn == '(eval)'
423
+ $stderr.puts 'unable to describe body for an eval block'
424
+ return
425
+ end
426
+
427
+ reg.source_fn = "#{fn}:#{lineno}"
428
+
429
+ File.open(fn) do |f|
430
+ source = ''
431
+ i = 0
432
+ ending = nil
433
+ found = false
434
+
435
+ while line = f.gets
436
+ i += 1
437
+ next unless ending || i == lineno
438
+ source << line
439
+ unless ending
440
+ unless m = line.match(/\{|do\b/)
441
+ $stderr.puts "unable to locate block beginning at #{fn}:#{lineno}"
442
+ return
443
+ end
444
+ ending = m[0] == '{' ? '\}' : 'end'
445
+ next
446
+ end
447
+
448
+ if m = line.match(/^#{ending}/)
449
+ found = true
450
+ break
451
+ end
452
+ end
453
+
454
+ if found
455
+ reg.source = source
456
+ else
457
+ reg.source = ''
458
+ end
459
+ end
460
+ end
461
+
462
+ if reg.source && !reg.source.empty?
463
+ puts '', "[#{reg.source_fn}]", '', reg.source
464
+ else
465
+ $stderr.puts "unable to locate body for #{sym}"
466
+ end
467
+ end
468
+
469
+ # Show all the pertinent version data we have about our
470
+ # software and the mysql connection.
471
+ #
472
+ def version # :doc:
473
+ puts "rsql:v#{RSQL::VERSION} client:v#{MySQLResults.conn.client_info} " \
474
+ "server:v#{MySQLResults.conn.server_info}"
475
+ end
476
+
477
+ # Show a short amount of information about acceptable syntax.
478
+ #
479
+ def help # :doc:
480
+ puts <<EOF
481
+
482
+ Converting values on the fly:
483
+
484
+ rsql> select name, value from rsql_example ! value => humanize_bytes;
485
+
486
+ Inspect MySQL connection:
487
+
488
+ rsql> . p [host_info, proto_info];
489
+
490
+ Escape strings:
491
+
492
+ rsql> . p escape_string('drop table "here"');
493
+
494
+ Show only rows containing a string:
495
+
496
+ rsql> select * from rsql_example | grep 'mystuff';
497
+
498
+ Show only rows containing a regular expression with case insensitive search:
499
+
500
+ rsql> select * from rsql_example | grep /mystuff/i;
501
+
502
+ EOF
503
+ end
504
+
505
+ # Provide a helper utility in the event a registered method would
506
+ # like to make its own queries.
507
+ #
508
+ def query(content, *args) # :doc:
509
+ MySQLResults.query(content, self, *args)
510
+ end
511
+
512
+ # Show the most recent queries made to the MySQL server in this
513
+ # session. Default is to show the last one.
514
+ #
515
+ def history(cnt=1)
516
+ if h = MySQLResults.history(cnt)
517
+ h.each{|q| puts '', q}
518
+ end
519
+ nil
520
+ end
521
+
522
+ # Call MySQLResults' grep to remove (or show) only those lines that
523
+ # have content matching the patterrn.
524
+ #
525
+ def grep(*args)
526
+ if @results.grep(*args)
527
+ @results
528
+ else
529
+ $stderr.puts 'No matches found'
530
+ end
531
+ end
532
+
533
+ # Register bangs to evaluate on all displayers as long as a column
534
+ # match is located. Bang keys may be either exact string matches or
535
+ # regular expressions.
536
+ #
537
+ def register_global_bangs(bangs)
538
+ @global_bangs.merge!(bangs)
539
+ end
540
+
541
+ # Exactly like register below except in addition to registering as
542
+ # a usable call for later, we will also use these as soon as we
543
+ # have a connection to MySQL.
544
+ #
545
+ def register_init(sym, *args, &block) # :doc:
546
+ register(sym, *args, &block)
547
+ @init_registrations << sym unless @init_registrations.include?(sym)
548
+ end
549
+
550
+ # If given a block, allow the block to be called later, otherwise,
551
+ # create a method whose sole purpose is to dynmaically generate
552
+ # sql with variable interpolation.
553
+ #
554
+ def register(sym, *args, &block) # :doc:
555
+ if m = caller.first.match(/^([^:]+:\d+)/)
556
+ source_fn = m[1]
557
+ end
558
+
559
+ name = usage = sym.to_s
560
+
561
+ if Hash === args.last
562
+ bangs = args.pop
563
+ desc = bangs.delete(:desc)
564
+ else
565
+ bangs = {}
566
+ end
567
+
568
+ desc = '' unless desc
569
+
570
+ if block.nil?
571
+ source = args.pop.strip
572
+ sql = squeeze!(source.dup)
573
+
574
+ argstr = args.join(',')
575
+ usage << "(#{argstr})" unless argstr.empty?
576
+
577
+ blockstr = %{lambda{|#{argstr}|%{#{sql}} % [#{argstr}]}}
578
+ block = Thread.new{ eval(blockstr) }.value
579
+ args = []
580
+ else
581
+ source = nil
582
+ usage << params(name, block)
583
+ end
584
+
585
+ @registrations[sym] = Registration.new(name, args, bangs, block, usage,
586
+ desc, source, source_fn)
587
+ end
588
+
589
+ # Convert a list of values into a comma-delimited string,
590
+ # optionally with each value in single quotes.
591
+ #
592
+ def to_list(vals, quoted=false) # :doc:
593
+ vals.collect{|v| quoted ? "'#{v}'" : v.to_s}.join(',')
594
+ end
595
+
596
+ # Convert a collection of values into hexadecimal strings.
597
+ #
598
+ def hexify(*ids) # :doc:
599
+ ids.collect do |id|
600
+ case id
601
+ when String
602
+ if id.start_with?('0x')
603
+ id
604
+ else
605
+ '0x' << id
606
+ end
607
+ when Integer
608
+ '0x' << id.to_s(16)
609
+ else
610
+ raise "invalid id: #{id.class}"
611
+ end
612
+ end.join(',')
613
+ end
614
+
615
+ # Convert a number of bytes into a human readable string.
616
+ #
617
+ def humanize_bytes(bytes) # :doc:
618
+ abbrev = ['B ','KB','MB','GB','TB','PB','EB','ZB','YB']
619
+ bytes = bytes.to_i
620
+ fmt = '%7.2f'
621
+
622
+ abbrev.each_with_index do |a,i|
623
+ if bytes < (1024**(i+1))
624
+ if i == 0
625
+ return "#{fmt % bytes} B"
626
+ else
627
+ b = bytes / (1024.0**i)
628
+ return "#{fmt % b} #{a}"
629
+ end
630
+ end
631
+ end
632
+
633
+ return bytes.to_s
634
+ end
635
+
636
+ # Convert a human readable string of bytes into an integer.
637
+ #
638
+ def dehumanize_bytes(str) # :doc:
639
+ abbrev = ['B','KB','MB','GB','TB','PB','EB','ZB','YB']
640
+
641
+ if str =~ /(\d+(\.\d+)?)\s*(\w+)?/
642
+ b = $1.to_f
643
+ if $3
644
+ i = abbrev.index($3.upcase)
645
+ return (b * (1024**i)).round
646
+ else
647
+ return b.round
648
+ end
649
+ end
650
+
651
+ raise "unable to parse '#{str}'"
652
+ end
653
+
654
+ # Show a nice percent value of a decimal string.
655
+ #
656
+ def humanize_percentage(decimal, precision=1) # :doc:
657
+ if decimal.nil? || decimal == 'NULL'
658
+ 'NA'
659
+ else
660
+ "%5.#{precision}f%%" % (decimal.to_f * 100)
661
+ end
662
+ end
663
+
664
+ # Convert a time into a relative string from now.
665
+ #
666
+ def relative_time(dt) # :doc:
667
+ return dt unless String === dt
668
+
669
+ now = Time.now.utc
670
+ theirs = Time.parse(dt + ' UTC')
671
+ if theirs < now
672
+ diff = now - theirs
673
+ postfix = 'ago'
674
+ else
675
+ diff = theirs - now
676
+ postfix = 'from now'
677
+ end
678
+
679
+ fmt = '%3.0f'
680
+
681
+ [
682
+ [31556926.0, 'years'],
683
+ [2629743.83, 'months'],
684
+ [86400.0, 'days'],
685
+ [3600.0, 'hours'],
686
+ [60.0, 'minutes']
687
+ ].each do |(limit, label)|
688
+ if (limit * 1.5) < diff
689
+ return "#{fmt % (diff / limit)} #{label} #{postfix}"
690
+ end
691
+ end
692
+
693
+ return "#{fmt % diff} seconds #{postfix}"
694
+ end
695
+
696
+ # Squeeze out any spaces.
697
+ #
698
+ def squeeze!(sql) # :doc:
699
+ sql.gsub!(/\s+/,' ')
700
+ sql.strip!
701
+ sql << ';' unless sql[-1] == ?;
702
+ sql
703
+ end
704
+
705
+ # Safely store an object into a file keeping at most one
706
+ # backup if the file already exists.
707
+ #
708
+ def safe_save(obj, name) # :doc:
709
+ name += '.yml' unless File.extname(name) == '.yml'
710
+ tn = "#{name}.tmp"
711
+ File.open(tn, 'w'){|f| YAML.dump(obj, f)}
712
+ if File.exist?(name)
713
+ bn = "#{name}~"
714
+ File.unlink(bn) if File.exist?(bn)
715
+ File.rename(name, bn)
716
+ end
717
+ File.rename(tn, name)
718
+ puts "Saved: #{name}"
719
+ end
720
+
721
+ def method_missing(sym, *args, &block)
722
+ if reg = @registrations[sym]
723
+ @bangs.merge!(reg.bangs)
724
+ final_args = reg.args + args
725
+ reg.block.call(*final_args)
726
+ elsif MySQLResults.respond_to?(sym)
727
+ MySQLResults.send(sym, *args)
728
+ elsif MySQLResults.conn.respond_to?(sym)
729
+ MySQLResults.conn.send(sym, *args)
730
+ else
731
+ super.method_missing(sym, *args, &block)
732
+ end
733
+ end
734
+
735
+ end # class EvalContext
736
+
737
+ end # module RSQL