# -*- Mode: ruby -*- # = Getting Starting with RSQL # This file is meant to be a working illustration of how RSQL might be used and # to show off various features of the application. # All examples below will use the following temporary table. You will need to # "use" a database first before loading this file since it will need to create # this temporary table. # @rsql_table = 'rsql_example' # To use this file, change directory to the one containing this file and run # RSQL connecting to your MySQL server (run rsql with no arguments for # usage--see {file:README.rdoc} for more details on command line parameters). # # rsql> .load 'example.rsqlrc'; # After it's loaded try listing out all the registered recipes (along with # parameter notes and descriptions). # # rsql> .list; # If you make changes to the example to try out new things (and please do!), you # can simply have the recipe file reloaded to have your changes pulled in # immediately without exiting your session. # # rsql> .reload; # Notice that any command issued starting with a period (.) results in # evaluation of Ruby. Thus, any valid Ruby syntax is applicable following a # period on a command. ################################################################################ # Use of {RSQL::EvalContext#register_init} allows a block to be automatically # invoked when this file is loaded. Often, this is useful to run set up routines # like setting MySQL variables for different read levels (e.g. SET # SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED). Any number of # these may be defined. # # Here we are merely setting up the example table. # register_init :setup_example, %q{ CREATE TEMPORARY TABLE IF NOT EXISTS #{@rsql_table} ( name VARCHAR(100), value INT(11), stuff BLOB ) }, :desc => 'Sets up example table for trying out RSQL.' # This next recipe is building up a string with a single variable interpolated # into it (our table name) through {RSQL::EvalContext#register}. The string will # then be used as if typed at the command line. # # rsql> .cleanup_example; # # In this case, we are simply dropping the table created by our initialization # recipe. If you do this, you'll need to call the setup_example # initialization recipe again before moving on. # # rsql> .setup_example; # register :cleanup_example, %q{ DROP TEMPORARY TABLE IF EXISTS #{@rsql_table} }, :desc => 'Cleans up the example table.' # This is an example of a recipe that utilizes a Ruby block for running code to # generate the SQL we eventually return. # # Here we are just populating the table (if it isn't already). # # rsql> .fill_table; # # Notice the use of the {RSQL::EvalContext#hexify} and {RSQL::EvalContext#squeeze!} # helper methods. # register :fill_table, :desc => 'Populate the example table.' do sql = '' 9.times do |i| sql << %{ INSERT IGNORE INTO #{@rsql_table} SET name='fancy#{i}', value=#{i**i}, stuff=#{hexify(rand((i+1)**100))}; } end # one more that isn't randomly generated so we can reference it later sql << %{ INSERT IGNORE INTO #{@rsql_table} SET name='fancy9', value=#{9**9}, stuff=0x1234567891234567891234567890; } squeeze!(sql) end # A very common reason for recipes is simply to add parameters to be dropped in # to our query. To facilitate this, declare one or more variables immediately # following the name of the recipe. These values can be listed by embedded # interpolation points into the string (just as you would with any Ruby string). # # This call will simply return results only for those bigger than some value # passed in. # # rsql> .get_big_values 80000; # register :get_big_values, :val, %q{ SELECT name, value FROM #{@rsql_table} WHERE #{val} <= value }, :desc => 'Get values bigger than the one provided as an argument.' # Sometimes we make mistakes (never!). Normally, the command history kept in # RSQL only stores the last thing entered at the prompt--not any query that the # previous command may have generated and invoked. When writing a recipe that # generates a query that has an error reported by MySQL, it is really handy to # see the generated SQL. # # Here's an example of a recipe that will fail. Run it and then hit the "up # arrow" key to see the previous command. # # rsql> .bad_query; # # So the command in our history is the recipe and not the query. To see the # query the {RSQL::EvalContext#history} has a helper method ready for us: # # rsql> .history; # register :bad_query, %q{ SELECT name, value FROM #{@rsql_table} WHERE valu < 10000 }, :desc => 'Make a query that will result in an error.' # After you have a table with content in it, you can run queries against it and # have the contents changed into something a little more meaningful. For # example, what if the values in our table were bytes that we wanted to # humanize? Try this command: # # rsql> select name, value from rsql_example ! value => humanize_bytes; # # The {RSQL::EvalContext#humanize_bytes} is another helper method. There are # several others available. Check out the {RSQL::EvalContext} class for details. # # Additional mappings can be added, separated by commas. # # You can also declare these column mappings in your recipes, though the syntax # is slightly different, using Ruby symbols. # # rsql> .show_values_as_bytes; # register :show_values_as_bytes, %q{ SELECT value FROM #{@rsql_table} }, 'value' => :humanize_bytes, :desc => 'Show values as humanized bytes.' # It is even possible to make up your own column mapping helpers. Just create a # Ruby method and reference it as a symbol mapped to whatever column the helper # is expecting for content. The return of the helper will be replaced as the # column entry's content. Your method is called once for each value in the # column from the results. # # rsql> .show_pretty_names; # # Make sure if your method doesn't understand the content passed to it that it # just reflects it back out so you don't lose data when printed. # def pretty_names(name) if m = name.match(/^(\w+)(\d+)$/) "#{m[1]} (#{m[2]})" else name end end register :show_pretty_names, %q{ SELECT name FROM #{@rsql_table} }, 'name' => :pretty_names, :desc => 'Show names separated to be more readable.' # It's also possible to work with the full set of query results in a recipe. # This can be useful if there is some coordination necessary across multiple # columns to result in some new kind of report. Much like a shell's ability to # pipe output from one command to the next, RSQL takes a similar approach. Try # this: # # rsql> select name, value from rsql_example | p @results; # # The {RSQL::EvalContext} class manages the results from a previous query in the # @results member variable accessible by any Ruby recipe # code. This is an instance of the {RSQL::MySQLResults} class. Below we make use # of the {RSQL::MySQLResults#each_hash} method to walk over all rows. There are # other helpful routines available. # # Here's an example that writes a simple report of the data we are working # with. To try this out, enter the following at the prompt: # # rsql> select name, value from rsql_example | to_report; # register :to_report, :desc => 'Report on a count of small and big values.' do small_cnt = 0 big_cnt = 0 @results.each_hash do |row| if row['value'].to_i < 10000 small_cnt +=1 else big_cnt += 1 end end puts "There are #{small_cnt} small values and #{big_cnt} big values." end # There may be other moments where it's necessary to take arguments, say if we # want to process results and keep our data around in a file. # # rsql> select name, value from rsql_example | save_values 'myobj'; # # After running this, a myobj.yml file should be created in the # local directory containing all the content from the query. To accomplish this, # the {RSQL::EvalContext#safe_save} method is invoked which serializes our # object so that we may later decided to run some post processing on the # content. # # Inspect the YAML[http://www.yaml.org/] content written out: # # rsql> .puts IO.read('myobj.yml'); # register :save_values, :desc => 'Save results from a query into a file.' do |fn| myobj = {} @results.each_hash do |row| myobj[row['name']] = row['value'] end safe_save(myobj, fn) end # Dealing with variable arguments is pretty straightforward as well, but with a # little syntactic twist. # # rsql> .find_names 'fancy3', 'fancy8'; # # Here we simply expand the arguments. # register :find_names, :'*names', %q{ SELECT name, value FROM #{@rsql_table} WHERE name IN (#{names.collect{|n| "'#{n}'"}.join(',')}) }, :desc => 'Find names from example table.' # Sometimes it just isn't enough to be able to rely on generating SQL queries # and piping into handlers. Sometimes we just need to roll up our sleeves and # run queries directly so we can start processing results and dealing with # presentation all on our own. That's where the {RSQL::EvalContext#query} helper # comes in handy. # # The intention here is to just create a series of sentences out of two separate # queries. # # rsql> .show_sentences; # register :show_sentences, :desc => 'Show results as sentences.' do query("SELECT name FROM #{@rsql_table}").each_hash do |nrow| name = nrow['name'] vals = query("SELECT value FROM #{@rsql_table} WHERE name='#{name}'") puts "The #{name} has #{vals[0]['value']} fanciness levels." end end # The {RSQL::MySQLResults} class built in to RSQL handles binary content # gracefully, automatically converting it to something a little nicer to our # consoles than just dumping it. It converts it into a hexadecimal string. # # rsql> SELECT stuff FROM rsql_example; # # The default is to limit the hex strings to 32 "bytes" reported. This can be # configured any time by setting the @hexstr_limit. # # RSQL makes querying for hex strings from within a recipe easy too. # # rsql> .find_stuff 0x1234567891234567891234567890; # register :find_stuff, :stuff, %q{ SELECT * FROM #{@rsql_table} WHERE stuff=#{hexify stuff} }, :desc => 'Find some hex stuff.' # There are many other things to try out left as an "exercise for the # reader". Browsing the {RSQL::EvalContext} and {RSQL::MySQLResults} classes # would be an excellent start. # vi: set filetype=ruby