Since the PAUSE form is not responding for me, please forgive this manual email...
Thanks, Phil R Lawrence prl -AT- cpan -DOT- org Name of the module SQL::Snippet Module List Chapter 11) String Processing, etc. Development Stage (Note: No implied timescales) b -- beta Support Level d -- developer Language Used p -- perl Interface Style O -- object oriented Public license p -- Standard Perl Description in Module List (44 chars limit) Enables an OO Interface to Your RDBMS Places where this module has been or will be discussed publicly clpm, YAPC::NA 2002 Modules with similar functionality SQL::QueryBuilder::Simple, Relations::Query Rationale OVERVIEW SQL::Snippet enables an OO (Object Oriented) interface to your RDBMS (Relational Database Management System). It does this by generating correct SQL based on your OO requests. Actual RDBMS-specific info (table names, joins, subselects, etc.) does not appear in your invocant script. While other modules exist that will create SQL for you, they do so only insofar as the invocant script passes RDBMS-specific info. SQL::Snippet is much simpler to use. You tell it you want SQL for 'this' or 'that', and it takes care of figuring out what tables and joins are involved. APPLICATION Reports and other scripts that use SQL often look like someone's first effort at web programming with Perl; only instead of being littered with HTML they are full of SQL: SQL that is duplicated in another script, and another, and another... So if it's a good thing to get your HTML out of your Perl scripts and into its own repository, why not do the same with your SQL? SQL::Snippet will require you to create a Perl module full of, well, snippets of SQL. For example, that snippet you use to limit a selection by gender. And that other one you use to grab all the miscreants that have never ordered from you even though they're still receiving your catalog. Wait, that's two snippets: the 'catalog_recipient' population and the 'never_ordered_a_single_thing' limit, which creates a subset of the 'catalog_recipient' population. The point is that, like most things, you can break those SQL statements down into their parts and store them in one central module, instead of in individual scripts. Then you can use SQL::Snippet to dynamically assemble these snippets as needed. This can even be done in an ad hoc manner; you can let the user build whatever question he likes of the RDBMS, and SQL::Snippet will take care of turning it into SQL. Once you have modularized your SQL into snippets, all you're left with is some easy OO syntax. PARADIGM There are four elements to the paradigm that SQL::Snippet uses to enable easy interaction with your RDBMS. 1. pop - Populations 2. lim - Limits applicable to those populations 3. parm - Parameters needed to flesh out the pops and lims 4. methods - each of the above three items has various built in methods associated with it. Additionally, objects and methods are automatically generated from the snippets in your Repository as needed. (See below.) Populations Population objects are real world things that are represented in your RDBMS. The following are example of objects you might find in your RDBMS: - people - suppliers - parts - assemblies - SKUs (Stock Keeping Units) All of these are real world things. Information about them in your RDBMS may be contained in one and only one table, or the information may be normalized (split) between many different tables. In the latter case, the SQL necessary to query your object of interest could get rather complicated, necessarily including all of the relevant tables and joins. SQL::Snippet abstracts the details of your RDBMS table structure into objects that you can simply reference in your scripts; SQL::Snippet will fill in all the details behind the scenes. For example: # prints the canonical SQL statement needed to # query info about assemblies from your RDBMS print $snippet->pop->assemblies->query; Limits Limits are types of real world attributes that can apply to the population objects. For example, gender is a type of attribute applicable to people, but not to assemblies. Weight is a type of attribute applicable to both people and assemblies. By indicating a limit for a given population, you can sharpen the definition of that population. For example: # apply the gender limit to our 'people' population $snippet->pop->people->lim->new( 'gender' ); Note that neither we nor the user of the script has yet specified what gender to limit the population by (Unknown, Female, etc.). We have only indicated that the population should have a gender limit applied to it. To complete the job we need to look at the next element of the SQL::Snippet paradigm. Parameters Parameters are the actual attributes used by population limits. In the above example we specified a gender limit, so now we should specify the gender parameter to be used by that limit. The gender limit requires only one parameter, aptly named 'gender'. # limit the people population to those with gender attribute of # Female or Unknown $snippet->pop->people->lim->gender->parm->gender( ['U','F'] ); Actually, we don't need to say: $snippet->pop->people->lim->new( 'gender' ); before saying: $snippet->pop->people->lim->gender->parm->gender( ['U','F'] ); The gender limit is autoinstantiated in the latter example. In fact, it's all autoinstantiated, from pop to gender. You start your script with a simple snippet object, and the rest autoinstantiates as needed. Methods In the above examples you see that pop, lim, and parm are autoinstantiated objects furnished by SQL::Snippet. There are multiple methods associated with each one. For example, we called the 'new' method on lim. But note that most of the methods and objects we used were actually named by us, the *users* of SQL::Snippet, not by me, the guy who wrote SQL::Snippet. For example, not only did we use the autoinstantiated 'gender' object, we also called the 'gender' method on parm (see the last example above). Note that we, the users of SQL::Snippet did not have to actually code a 'gender' method somewhere -- far from it. This method was AUTOLOADed for us. All we had to do was create a repository with pop, lim, and parm snippets, and the rest was automatic. For example, here is the 'gender' snippet from SQL::Snippet::ExampleRepository, an example repository included with the distribution: if ($parm eq 'gender') { return ( name => $parm, default => '', msg => "\nBy default, gender will bear no impact " . "on selection. Press ENTER to accept " . "this default, or, to limit the selection " . "by gender, type one or more gender codes " . "from the following list: M (Male), " . "F (Female), U (Unknown)", delimiter => ',', prompt => 'Gender', list_check => [ 'M','F','U' ], ); } elsif ($parm eq ... When you reference 'gender' in your code, either as an object or method (as in the above examples), SQL::Snippet will automatically create the needed object or method using this snippet as a source of basic information. If you can follow this easy pattern for creating snippets, you can use SQL::Snippet. SQL::Snippet itself automatically creates the back end stuff needed to allow the use of intuitive OO syntax. HOW I USE IT I use SQL::Snippet in a production system with seven basic populations that my clients like to report on. I then have 31 different limits. Multiple limits can, of course, be combined with a population. Not every limit is compatible with all base populations, but most are, leaving the client with more possible questions they can ask of the RDBMS than I have statistical chops to calculate. :-) To make it all work I have 47 different parameters. Some limits only require one parameter (i.e. the gender limit only needs one parameter -- gender), but some require more than one. Also, many lim snippets need access to extra parms in order to correctly tie into the population. For example, if the gender information in the database is stored in table not included in the chosen pop snippet, the gender lim snippet will also need something like 'base_table' and 'linking_field' parms to correctly link itself in to the base population. Most of the populations are actually pre-limited (i.e. the populations don't refer to so basic an object as 'people', but rather to different kinds of people). Thus most require more than one parameter and tend to have multiple tables included in their definition. My clients love their ability to ask anything they can think of from the RDBMS; I love the ability to maintain the SQL snippets in one and only one place. EXAMPLE SCRIPT 1 # specify the repository to be used. The repository is # subclassed from SQL::Snippet. use SQL::Snippet::ExampleRepository; # If you don't specify the parm values needed to fill out # your pops and lims, the user will be prompted automatically. # I use Term::Interact to enable this in interaction in a # command line environment. use Term::Interact; # We'll use DBI to execute the SQL we get from SQL::Snippet. # Also, SQL::Snippet may use DBI to validate user input (if # you so specify in the repository) and to quote parameters. use DBI; my $ti = Term::Interact->new; my $dbh = DBI->connect( ... ); my $snippet = SQL::Snippet::ExampleRepository->new( dbh => $dbh, ui => $ti, syntax => 'oracle' ); ###EXAMPLE 1### # We need to specify what our SELECT statement will be. # A future version will allow prompting of the user to # build his own SELECT based on meta-data supplied by # the snippets. $snippet->pop->catalog_recipient->select( 'SELECT count(*)' ); # get the SQL necessary to pull a count of the catalog # recipient population out of your RDBMS. print $snippet->pop->catalog_recipient->query; ###EXAMPLE 2### # instead of grabbing the whole population of catalog # recipients, let's limit it to those who never placed # an order. $snippet->pop->catalog_recipient->lim->last_order->parm->last_order( 'null' ); # our SELECT is still set for this pop from above... print $snippet->pop->catalog_recipient->query; ###EXAMPLE 3### # instead of forcing a last_order limit, let's let the user # say whether or not he wants the limit, and if so what sort # of limit. Possible inputs by the user might be: # '> 1/1/2001' # with orders after 1/1/2001 # -or- # 'null' # with no orders ever # -or- # '' # let's not limit, thank you # set the limit for this pop $snippet->pop->catalog_recipient->lim->new( 'last_order' ); # when generating the SQL, SQL::Snippet will notice we have # requested a 'last_order' limit, but no parm value for # 'last_order' has been set. The user will be prompted with # whatever verbiage we have stored in the repository, and # their input will be parsed by whatever logic we have in the # repository. (See perldoc Term::Interact for the details # of user prompting, and the source for SQL::Snippet:: # ExampleRepository for boilerplate and example Repository # logic. print $snippet->pop->catalog_recipient->query; EXAMPLE SCRIPT 2 # this example script uses some more advanced functionality. # see perldoc SQL::Snippet for full documentation. use SQL::Snippet::ExampleRepository; use Term::Interact; use DBI; my $ti = Term::Interact->new; my $dbh = DBI->connect( ... ); my $snippet = SQL::Snippet::ExampleRepository->new( dbh => $dbh, ui => $ti, syntax => 'oracle' ); # all pops referenced from the current snippet will # automatically have the zip limit applied. $snippet->shared_lim->new( 'zip' ); # Since all pops will share the zip lim, let's set # the zip parm value *once* the top level (instead # of once for each pop at the pop->lim->parm level). # Here, the value will be set via user interaction # since we have passed in no value and none has # been previously set. $snippet->parm->zip->value; $snippet->pop->pre_sale->select( 'SELECT SKU, count(SKU)' ); $snippet->pop->pre_sale->group_by( 'SKU' ); $snippet->pop->pre_sale->order_by( 'count(SKU)' ); $snippet->pop->sale->select( 'SELECT SKU, count(SKU)' ); $snippet->pop->sale->group_by( 'SKU' ); $snippet->pop->sale->order_by( 'count(SKU)' ); $snippet->pop->re_sale->select( 'SELECT SKU, count(SKU)' ); $snippet->pop->re_sale->group_by( 'SKU' ); $snippet->pop->re_sale->order_by( 'count(SKU)' ); my $pre_sale_hits_by_SKU = $dbh->selectall_arrayref( $snippet->pop->pre_sale->query ); my $sales_by_SKU = $dbh->selectall_arrayref( $snippet->pop->sale->query ); my $re_sales_by_SKU = $dbh->selectall_arrayref( $snippet->pop->re_sale->query ); print " ----SKU---- --COUNT--\n"; print "Pre-Sales Hits\n"; for (@$pre_sale_hits_by_SKU) { print " $_->[0] $_->[1]\n"; } print "Sales\n"; for (@$sales_by_SKU) { print " $_->[0] $_->[1]\n"; } print " Re-Sales\n"; for (@$re_sales_by_SKU) { print " $_->[0] $_->[1]\n"; } # print any notes the repository has associated with # limits placed on the whole report. print scalar($snippet->get_shared_lim_notes);