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