rsql 0.2.7 → 0.2.8

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2011-2012 by 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.
@@ -0,0 +1,291 @@
1
+ # -*- Mode: ruby -*-
2
+
3
+ # This file is meant to be a working illustration of how RSQL might be
4
+ # used and to show off various features of the application.
5
+
6
+ # All examples below will use this temporary table. You will need to
7
+ # "use" a database first before loading this file since it will need
8
+ # to create this temporary table.
9
+ #
10
+ @rsql_table = 'rsql_example'
11
+
12
+ # To use this file, change directory to the one containing this file,
13
+ # run rsql connecting to your MySQL server (run rsql with no arguments
14
+ # for usage).
15
+ #
16
+ # rsql> .load 'example.rsqlrc';
17
+
18
+ # After it's loaded try listing out all the registered recipes (along
19
+ # with parameter notes and descriptions).
20
+ #
21
+ # rsql> .list;
22
+
23
+ # If you make changes to the example to try out new things (and please
24
+ # do!), you can simply have the recipe file reloaded to have your
25
+ # changes pulled in immediately without exiting your session.
26
+ #
27
+ # rsql> .reload;
28
+
29
+ # Notice that any command issued starting with a period (.) results in
30
+ # evaluation of Ruby. Thus, any valid Ruby syntax is applicable
31
+ # following a period on a command.
32
+
33
+ ################################################################################
34
+
35
+ # This type of registration is automatically invoked when this file is
36
+ # loaded. Often, this is useful to run set up routines like setting
37
+ # MySQL variables for different read levels (e.g. SET SESSION
38
+ # TRANSACTION ISOLATION LEVEL READ COMMITTED). Any number of these may
39
+ # be defined.
40
+ #
41
+ # Here we are merely setting up the example table.
42
+ #
43
+ register_init :setup_example, %q{
44
+ CREATE TEMPORARY TABLE IF NOT EXISTS #{@rsql_table} (
45
+ name VARCHAR(100),
46
+ value INT(11),
47
+ stuff BLOB
48
+ )
49
+ }, :desc => 'Sets up example table for trying out RSQL.'
50
+
51
+ # This recipe is simply building up a string with a single variable
52
+ # interpolated into it (our table name). The string will then be used
53
+ # as if typed at the command line.
54
+ #
55
+ # rsql> .cleanup_example;
56
+ #
57
+ # In this case, we are simply dropping the table created by our
58
+ # initialization recipe. If you do this, you'll need to call the
59
+ # setup_example initialization recipe again before moving on.
60
+ #
61
+ # rsql> .setup_example;
62
+ #
63
+ register :cleanup_example, %q{
64
+ DROP TEMPORARY TABLE IF EXISTS #{@rsql_table}
65
+ }, :desc => 'Cleans up the example table.'
66
+
67
+ # This is an example of a recipe that utilizes a Ruby block for
68
+ # running code to generate the SQL we eventually return.
69
+ #
70
+ # Here we are just populating the table (if it isn't already).
71
+ #
72
+ # rsql> .fill_table;
73
+ #
74
+ # Notice the use of hexify and squeeze! methods available from
75
+ # EvalContext.
76
+ #
77
+ register :fill_table, :desc => 'Populate the example table.' do
78
+ sql = ''
79
+ 9.times do |i|
80
+ sql << %{
81
+ INSERT IGNORE INTO #{@rsql_table}
82
+ SET name='fancy#{i}',
83
+ value=#{i**i},
84
+ stuff=#{hexify(rand((i+1)**100))};
85
+ }
86
+ end
87
+ # one more that isn't randomly generated so we can reference it
88
+ # later
89
+ sql << %{
90
+ INSERT IGNORE INTO #{@rsql_table}
91
+ SET name='fancy9',
92
+ value=#{9**9},
93
+ stuff=0x1234567891234567891234567890;
94
+ }
95
+ squeeze!(sql)
96
+ end
97
+
98
+ # A very common reason for recipes is simply to add parameters to be
99
+ # dropped in to our query. To facilitate this, simply declare one or
100
+ # more variables immediately following the name of the recipe. Then
101
+ # these values can be listed by embedded interpolation points into the
102
+ # string (just as you would with any Ruby string).
103
+ #
104
+ # This call will simply return results only for those bigger than some
105
+ # value passed in.
106
+ #
107
+ # rsql> .get_big_values 80000;
108
+ #
109
+ register :get_big_values, :val, %q{
110
+ SELECT name, value FROM #{@rsql_table} WHERE #{val} <= value
111
+ }, :desc => 'Get values bigger than the one provided as an argument.'
112
+
113
+ # Sometimes we make mistakes (never!). Normally, the command history
114
+ # kept in RSQL only stores the last thing entered at the prompt--not
115
+ # any query that the previous command may have generated and invoked.
116
+ # When writing a recipe that generates a query that has an error
117
+ # reported by MySQL, it is really handy to see the query.
118
+ #
119
+ # Here's an example of a recipe that will fail. Run it and then hit the
120
+ # "up arrow" key to see the previous command.
121
+ #
122
+ # rsql> .bad_query;
123
+ #
124
+ # So the command in our history is the recipe and not the query. To
125
+ # see the query the EvalContext has a recipe ready for us:
126
+ #
127
+ # rsql> .history;
128
+ #
129
+ register :bad_query, %q{
130
+ SELECT name, value FROM #{@rsql_table} WHERE valu < 10000
131
+ }, :desc => 'Make a query that will result in an error.'
132
+
133
+ # After you have a table with content in it, you can run queries
134
+ # against it and have the contents changed into something a little
135
+ # more meaningful. For example, what if the values in our table were
136
+ # bytes that we wanted to humanize? Try this command:
137
+ #
138
+ # rsql> select name, value from rsql_example ! value => humanize_bytes;
139
+ #
140
+ # The humanize_bytes method is a helper in the EvalContext
141
+ # class. There are several others available. Check out the rdoc for
142
+ # details.
143
+ #
144
+ # Additional mappings can be added, separated by commas.
145
+ #
146
+ # You can also declare these column mappings in your recipes, though
147
+ # the syntax is slightly different, using Ruby symbols.
148
+ #
149
+ # rsql> .show_values_as_bytes;
150
+ #
151
+ register :show_values_as_bytes, %q{
152
+ SELECT value FROM #{@rsql_table}
153
+ }, 'value' => :humanize_bytes,
154
+ :desc => 'Show values as humanized bytes.'
155
+
156
+ # It is even possible to make up your own column mapping helpers. Just
157
+ # create a Ruby method and reference it as a symbol mapped to whatever
158
+ # column the helper is expecting for content. The return of the helper
159
+ # will be replaced as the column entry's content. Your method is
160
+ # called once for each value in the column from the results.
161
+ #
162
+ # rsql> .show_pretty_names;
163
+ #
164
+ # Make sure if your method doesn't understand the content passed to it
165
+ # that it just reflects it back out so you don't lose data when
166
+ # printed.
167
+ #
168
+ def pretty_names(name)
169
+ if m = name.match(/^(\w+)(\d+)$/)
170
+ "#{m[1]} (#{m[2]})"
171
+ else
172
+ name
173
+ end
174
+ end
175
+
176
+ register :show_pretty_names, %q{
177
+ SELECT name FROM #{@rsql_table}
178
+ }, 'name' => :pretty_names,
179
+ :desc => 'Show names separated to be more readable.'
180
+
181
+ # It's also possible to work with the full set of query results in a
182
+ # recipe. This can be useful if there is some coordination necessary
183
+ # across multiple columns to result in some new kind of report. Much
184
+ # like a shell's ability to pipe output from one command to the next,
185
+ # RSQL takes a similar approach. Try this:
186
+ #
187
+ # rsql> select name, value from rsql_example | p @results;
188
+ #
189
+ # The EvalContext manages the results from a previous query in the
190
+ # @results member variable accessible by any Ruby recipe code. This is
191
+ # an instance of the MySQLResults class. Below we make use of the
192
+ # each_hash method to walk over all rows. There are other helpful
193
+ # routines available as well that are documented in rdoc.
194
+ #
195
+ # Here's an example that writes a simple report of the data we are
196
+ # working with. To try this out, enter the following at the prompt:
197
+ #
198
+ # rsql> select name, value from rsql_example | to_report;
199
+ #
200
+ register :to_report, :desc => 'Report on a count of small and big values.' do
201
+ small_cnt = 0
202
+ big_cnt = 0
203
+ @results.each_hash do |row|
204
+ if row['value'].to_i < 10000
205
+ small_cnt +=1
206
+ else
207
+ big_cnt += 1
208
+ end
209
+ end
210
+ puts "There are #{small_cnt} small values and #{big_cnt} big values."
211
+ end
212
+
213
+ # There may be other moments where it's necessary to take arguments,
214
+ # say if we want to process results and keep our data around in a
215
+ # file.
216
+ #
217
+ # rsql> select name, value from rsql_example | save_values 'myobj';
218
+ #
219
+ # After running this, a myobj.yml file should be created in the local
220
+ # directory containing all the content from the query. To accomplish
221
+ # this, the use of EvalContext's safe_save method is invoked which
222
+ # serializes our object so that we may later decided to run some post
223
+ # processing on the content.
224
+ #
225
+ # Inspect the YAML content written out:
226
+ #
227
+ # rsql> .puts IO.read('myobj.yml');
228
+ #
229
+ register :save_values, :desc => 'Save results from a query into a file.' do |fn|
230
+ myobj = {}
231
+ @results.each_hash do |row|
232
+ myobj[row['name']] = row['value']
233
+ end
234
+ safe_save(myobj, fn)
235
+ end
236
+
237
+ # Dealing with variable arguments is pretty straightforward as well,
238
+ # but with a little syntactic twist.
239
+ #
240
+ # rsql> .find_names 'fancy3', 'fancy8';
241
+ #
242
+ # Here we simply expand the arguments.
243
+ #
244
+ register :find_names, :'*names', %q{
245
+ SELECT name, value
246
+ FROM #{@rsql_table}
247
+ WHERE name IN (#{names.collect{|n| "'#{n}'"}.join(',')})
248
+ }, :desc => 'Find names from example table.'
249
+
250
+ # Sometimes it just isn't enough to be able to rely on generating SQL
251
+ # queries and piping into handlers. Sometimes we just need to roll up
252
+ # our sleeves and run queries directly so we can start processing
253
+ # results and dealing with presentation all on our own. That's where
254
+ # EvalContext's query helper comes in handy.
255
+ #
256
+ # The intention here is to just create a series of sentences out of
257
+ # two separate queries.
258
+ #
259
+ # rsql> .show_sentences;
260
+ #
261
+ register :show_sentences, :desc => 'Show results as sentences.' do
262
+ query("SELECT name FROM #{@rsql_table}").each_hash do |nrow|
263
+ name = nrow['name']
264
+ vals = query("SELECT value FROM #{@rsql_table} WHERE name='#{name}'")
265
+ puts "The #{name} has #{vals[0]['value']} fanciness levels."
266
+ end
267
+ end
268
+
269
+ # The MySQLResults class built in to RSQL handles binary content
270
+ # gracefully, automatically converting it to something a little nicer
271
+ # to our consoles than just dumping it. It converts it into a
272
+ # hexadecimal string.
273
+ #
274
+ # rsql> SELECT stuff FROM rsql_example;
275
+ #
276
+ # The default is to limit the hex strings to 32 "bytes" reported. This
277
+ # can be configured any time by setting the @hexstr_limit.
278
+ #
279
+ # RSQL makes querying for hex strings from within a recipe easy too.
280
+ #
281
+ # rsql> .find_stuff 0x1234567891234567891234567890;
282
+ #
283
+ register :find_stuff, :stuff, %q{
284
+ SELECT * FROM #{@rsql_table} WHERE stuff=#{hexify stuff}
285
+ }, :desc => 'Find some hex stuff.'
286
+
287
+ # There are many other things to try out left as an "exercise for the
288
+ # reader". Browsing the rdoc for EvalContext and MySQLResults would be
289
+ # an excellent start.
290
+
291
+ # vi: set filetype=ruby
@@ -0,0 +1,10 @@
1
+ # A module encapsulating classes to manage MySQLResults and process
2
+ # Commands using an EvalContext for handling recipes.
3
+ #
4
+ module RSQL
5
+ VERSION = '0.2.8'
6
+
7
+ require 'rsql/mysql_results'
8
+ require 'rsql/eval_context'
9
+ require 'rsql/commands'
10
+ end
@@ -0,0 +1,243 @@
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 'stringio'
25
+
26
+ EvalResults = Struct.new(:value, :stdout)
27
+
28
+ ########################################
29
+ # A wrapper to parse and handle commands
30
+ #
31
+ class Commands
32
+
33
+ Command = Struct.new(:content, :bangs, :declarator, :displayer)
34
+
35
+ ########################################
36
+
37
+ # Split commands on these characters.
38
+ SEPARATORS = ';|!'
39
+
40
+ # Split on separators, allowing for escaping;
41
+ #
42
+ def initialize(input, default_displayer)
43
+ @default_displayer = default_displayer
44
+ @cmds = []
45
+ esc = ''
46
+ bangs = {}
47
+ match_before_bang = nil
48
+ in_pipe_arg = false
49
+ next_is_ruby = false
50
+
51
+ input.scan(/[^#{SEPARATORS}]+.?/) do |match|
52
+ orig_match = match
53
+
54
+ if i = SEPARATORS.index(match[-1])
55
+ sep = SEPARATORS[i]
56
+ match.chop!
57
+
58
+ if match[-1] == ?\\
59
+ # unescape the separator and save the content away
60
+ esc << match[0..-2] << sep
61
+ next
62
+ end
63
+ else
64
+ sep = nil
65
+ end
66
+
67
+ unless esc.empty?
68
+ esc << match
69
+ match = esc
70
+ esc = ''
71
+ end
72
+
73
+ if match_before_bang
74
+ new_bangs = {}
75
+ match.split(/\s*,\s*/).each do |ent|
76
+ (key,val) = ent.split(/\s*=>\s*/)
77
+ unless key && val
78
+ # they are using a bang but have no maps
79
+ # so we assume this is a != or something
80
+ # similar and let it go through unmapped
81
+ esc = match_before_bang + '!' + match
82
+ match_before_bang = nil
83
+ break
84
+ end
85
+ if val.strip == 'nil'
86
+ new_bangs[key.strip] = nil
87
+ else
88
+ new_bangs[key.strip] = val.to_sym
89
+ end
90
+ end
91
+ next unless match_before_bang
92
+ match = match_before_bang
93
+ match_before_bang = nil
94
+ bangs.merge!(new_bangs)
95
+ end
96
+
97
+ if sep == ?!
98
+ match_before_bang = match
99
+ next
100
+ end
101
+
102
+ if sep == ?|
103
+ # we've split on a pipe so we need to handle the
104
+ # case where ruby code is declaring a block with
105
+ # arguments (e.g. {|x| p x} or do |x| p x end)
106
+ if in_pipe_arg
107
+ in_pipe_arg = false
108
+ esc << match << '|'
109
+ next
110
+ elsif orig_match =~ /\{\s*|do\s*/
111
+ in_pipe_arg = true
112
+ esc << match << '|'
113
+ next
114
+ end
115
+ end
116
+
117
+ add_command(match, bangs, next_is_ruby, sep)
118
+
119
+ bangs = {}
120
+ next_is_ruby = sep == ?|
121
+ end
122
+
123
+ add_command(esc, bangs, next_is_ruby)
124
+ end
125
+
126
+ def empty?
127
+ return @cmds.empty?
128
+ end
129
+
130
+ def concat(other)
131
+ @cmds.concat(other)
132
+ end
133
+
134
+ def last
135
+ @cmds.last
136
+ end
137
+
138
+ def run!(eval_context)
139
+ last_results = nil
140
+ while @cmds.any?
141
+ cmd = @cmds.shift
142
+ results = run_command(cmd, last_results, eval_context)
143
+ return :done if results == :done
144
+
145
+ if cmd.displayer == :pipe
146
+ last_results = results
147
+ elsif MySQLResults === results
148
+ last_results = nil
149
+ results.send(cmd.displayer)
150
+ elsif EvalResults === results
151
+ last_results = nil
152
+ if MySQLResults === results.value
153
+ # This happens if their recipe returns MySQL
154
+ # results...just display it like above.
155
+ results.value.send(cmd.displayer)
156
+ else
157
+ if results.stdout && 0 < results.stdout.size
158
+ puts results.stdout.string
159
+ end
160
+ puts "=> #{results.value.inspect}" if results.value
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ ########################################
167
+ private
168
+
169
+ def add_command(content, bangs, is_ruby, separator=nil)
170
+ content.strip!
171
+
172
+ case content[0]
173
+ when ?.
174
+ content.slice!(0)
175
+ declarator = :ruby
176
+ else
177
+ declarator = is_ruby ? :ruby : nil
178
+ end
179
+
180
+ if content.end_with?('\G')
181
+ # emulate mysql's \G output
182
+ content.slice!(-2,2)
183
+ displayer = :display_by_line
184
+ elsif separator == ?|
185
+ displayer = :pipe
186
+ else
187
+ displayer = @default_displayer
188
+ end
189
+
190
+ unless content.empty?
191
+ @cmds << Command.new(content, bangs, declarator, displayer)
192
+ return true
193
+ end
194
+
195
+ return false
196
+ end
197
+
198
+ def run_command(cmd, last_results, eval_context)
199
+ eval_context.bangs = cmd.bangs
200
+
201
+ if cmd.declarator
202
+ stdout = cmd.displayer == :pipe ? StringIO.new : nil
203
+ value = eval_context.safe_eval(cmd.content, last_results, stdout)
204
+ if String === value
205
+ cmds = Commands.new(value, cmd.displayer)
206
+ unless cmds.empty?
207
+ # need to carry along the bangs into the
208
+ # last command so we don't lose them
209
+ if cmds.last.bangs.empty? && cmd.bangs.any?
210
+ cmds.last.bangs = cmd.bangs
211
+ end
212
+ @cmds = cmds.concat(@cmds)
213
+ end
214
+ return
215
+ end
216
+ else
217
+ value = cmd.content
218
+ end
219
+
220
+ return :done if value == 'exit' || value == 'quit'
221
+
222
+ if String === value
223
+ begin
224
+ last_results = MySQLResults.query(value, eval_context)
225
+ rescue MySQLResults::MaxRowsException => ex
226
+ $stderr.puts "refusing to process #{ex.rows} rows (max: #{ex.max})--" <<
227
+ "consider raising this via set_max_rows"
228
+ rescue Mysql::Error => ex
229
+ $stderr.puts ex.message
230
+ rescue Exception => ex
231
+ $stderr.puts ex.inspect
232
+ raise
233
+ end
234
+ else
235
+ last_results = EvalResults.new(value, stdout)
236
+ end
237
+
238
+ return last_results
239
+ end
240
+
241
+ end # class Commands
242
+
243
+ end # module RSQL