After making a couple of posts on http://use.perl.org, I'm posting my code here, maybe for eventual posting to CPAN, and asking for comments (on code, module name, docs, etc.), questions, and whatever else may come. As the example (toward the bottom of the docs) indicates, this is for building a big ugly nested foreach loop for SQL statements, hopefully without quite so much bigness and ugliness. Iterator-DBI was too simple for my needs, and yet required more kerfluffery in the Iterator department (with is_exhaused methods, etc.). It would be simple to wrap an iterator created with this module into something that Iterator could use. Anyway, here's the code:
Update: One thing I will change is to default to prepare_cached(), and provide an option to fallback to prepare()
Another update: This uses fetchall_arrayref(), which suits my needs just fine. But if you were selecting millions of rows from one of these iterators (that is, one of the sub-iterators, not the master iterator created by mk_iterator), that may not be a good thing. So a possible option (which would require some work) would be to provide an option to fetch one row at a time instead of fetchall.
package DBIx::Iterator; use strict; use warnings; use base 'Exporter'; our @EXPORT_OK = qw(mk_iterator list_iterator sql_iterator); sub sql_iterator { my %cfg = @_; my ( $sql, $args, $select, $binds, $transform, $dbh, $master, $out +er, $cache) = @cfg{qw(SQL ARGS SELECT BIND TRANSFORM DBH MASTER OUT +ER CACHE)}; $args ||= []; my @arg = ( undef, @$args ); my @bind; if ( defined $binds ) { @bind = ref($binds) ? ( undef, @$binds ) : ( $binds ) x @arg; } my $key; my $master_cnt; my $nxt_master; my $rows = []; sub { until (@$rows) { if ( $master ) { $nxt_master = $master->() or return; } else { return if $master_cnt++; $nxt_master = {}; } my $sth = $dbh->prepare($sql); my $cache_hit; if ( $cache ) { no warnings 'uninitialized'; $key = join $;, @$nxt_master{@$args}; if ( exists( $cache->{$key} ) ) { $rows = [ @{$cache->{$key}} ]; $cache_hit = 1; } } unless ( $cache_hit ) { if ( defined $binds ) { $sth->bind_param($_, $nxt_master->{$arg[$_]}, $bind[$_]) for + 1..$#arg; } else { $sth->bind_param($_, $nxt_master->{$arg[$_]}) for 1..$#arg; } $sth->execute(); $rows = $sth->fetchall_arrayref(); $rows = $transform->($rows,$nxt_master,\%cfg) if $transform; $cache->{$key} = [ @$rows ] if $cache; } $rows = [ [] ] if $outer and !@$rows; } my $nxt_row = shift @$rows; @$nxt_master{@$select} = @$nxt_row; return $nxt_master; } } sub mk_iterator { my %cfg; if ( @_ and UNIVERSAL::isa($_[0], 'HASH') ) { %cfg = %{shift(@_)}; } my $f; while ( my $nxt_f = shift(@_) ) { if ( UNIVERSAL::isa($nxt_f, 'ARRAY' ) ) { my @join = $nxt_f; push @join, shift(@_) while @_ and UNIVERSAL::isa($_[0], 'ARRAY' + ); my $new_cfg = { %cfg }; $new_cfg->{MASTER} = $f; $f = join_iterators($new_cfg, @join); } else { my %override = ( @_ and UNIVERSAL::isa($_[0], 'HASH')) ? %{shift +(@_)} : (); my %new_cfg = %cfg; $new_cfg{MASTER} = $f if $f; $f = $nxt_f->(%new_cfg, %override); } } return $f; } sub join_iterators { my %cfg; if ( @_ and UNIVERSAL::isa($_[0], 'HASH') ) { %cfg = %{shift(@_)}; } my ($master, $dbh) = @cfg{qw(MASTER DBH)}; my @mk_iter = @_; my $nxt_master; my $master_cnt; my @master_list; # Transform master iterator into one which returns # a 'more' indicator, followed by the value and an # end flag for each iterator in the list. my $new_master = sub { unless ( @master_list ) { if ( $master ) { my $nxt = $master->() or return; @master_list = ( $nxt, ($nxt, undef ) x @mk_iter ); } else { return if $master_cnt++; @master_list = ( {}, ( {}, undef ) x @mk_iter ); } } my $nxt_master = shift @master_list; return unless $nxt_master; return { %$nxt_master }; }; my $new_cfg = { %cfg, MASTER => $new_master }; my @iter = map { mk_iterator( $new_cfg, @$_ ) } @mk_iter; my $idx = @mk_iter; # Exhaust every iterator before seeing if master # iterator has more values. sub { { if ( $idx >= @mk_iter ) { my $tmp = $new_master->() or return; $idx = 0; } my $nxt = $iter[$idx]->(); return $nxt if $nxt; $idx++; redo; } } } sub list_iterator { my %cfg = @_; my ( $list,$select, $transform, $master, $outer) = @cfg{qw(LIST SELECT TRANSFORM MASTER OUTER)}; my $master_cnt; my $nxt_master; my $rows = []; sub { until (@$rows) { if ( $master ) { $nxt_master = $master->() or return; } else { return if $master_cnt++; $nxt_master = {}; } $rows = [ @$list ]; $rows = $transform->($rows,$nxt_master,\%cfg) if $transform; $rows = [ [] ] if $outer and !@$rows; } my $nxt_row = shift @$rows; @$nxt_master{@$select} = @$nxt_row; return $nxt_master; } } 1; __END__ =head1 NAME DBIx::Iterator - An iterator package for SQL statements =head1 SYNOPSIS use DBIx::Iterator qw(list_iterator sql_iterator mk_iterator); sub iter_generator1 { sql_iterator( SQL => $sql_statement, %other_options, @_, ) }; sub list_iter_generator { list_iterator( LIST => [ @list_of_lists ], \%more_options, ) } my $iterator = mk_iterator( \%main_options, \&iter_generator1, \%more_options, \&iter_generator2, \&iter_generator3, etc. ); or: my $iterator = mk_iterator( \%main_options, \&iter_generator1, \%iter_generator1_arguments, [ \&iter_generator2, \&iter_generator3, etc. ], [ \&iter_generator4, \&iter_generator5, etc. ], etc. ); while ( my $row = $iterator->() ) { # $row is a hashref } =head1 DESCRIPTION This package allows you to have a nested SQL iterator without having t +o build nested for loops. Nothing is exported by default, but functions sql_iterator and mk_iterator are exported if explicitly requested. =over 4 =item mk_iterator Returns an iterator function which will return a hash reference on eve +ry call. The first argument is a hash reference, usually containing the databas +e handle, and all following arguments are individual iterator generators that optionally accept a configuration +hash. Each iterator generated is passed to the next generator in the 'MASTER' slot in the configurat +ion hash, and the database handle is passed to every iterator in the 'DBH' slot. The argument following each generator may be a hash reference, which w +ill be passed to the generator which can be used, e.g., to override the generator's def +ault configuration. =item sql_iterator Returns an iterator. Usually used inside an iterator generator functio +n. Accepts a hash, whose keys may be: SQL - The sql statement. ARGS - Array ref of hash keys corresponding to '?' placeholder argumen +ts in the sql. BIND - Array ref of DBI data types used to bind the ARGS to the sql st +atement. Or a scalar containing a single type to bind to all arguments. SELECT - Keys to add to the hashref returned from the iterator. Usuall +y corresponds to the select clause in the sql statement, but names may be different. TRANSFORM - A subroutine that takes all rows returned by the sql in an + arrayref, and returns a new array ref of rows transformed. May be used to 'flatt +en' the results of a sql statement. OUTER - Like an outer join. If no rows are returned by the sql stateme +nt, return the master row once before fetching a new master row. DBH - The database handle. Usually not required, as it is passed in by + default via mk_iterator(). MASTER - The parent iterator. Usually not required, as it is passed in + by default via mk_iterator(). CACHE - A hashref used to cache the selected data (key is the data cor +responding to the ARGS option joined by $SUBSEP). =back =head1 EXAMPLES sub get_customers { mk_sql_iterator( SQL => 'select cust_id, cust_name from customers', SELECT => [qw(CUST_ID CUSTOMER_NAME)], @_, ); } sub get_orders { mk_sql_iterator( SQL => 'select order_no, order_amt from orders where cust_id = ? +' SELECT => [qw(ORDER_NO TOTAL_AMT)], ARGS => ['CUST_ID'], @_, ); } my $f = mk_iterator( { DBH => $dbh }, \&get_customers, \&get_orders, ); while (my $order = $f->()) { # $order is hash ref with keys CUST_ID, CUSTOMER_NAME, # ORDER_NO, and TOTAL_AMT } =head1 AUTHOR runrig =head1 COPYRIGHT This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 SEE ALSO L<Iterator>. =cut =cut
|
|---|
| Replies are listed 'Best First'. | |
|---|---|
|
Re: RFC: DBIx::Iterator
by runrig (Abbot) on May 12, 2007 at 00:48 UTC | |
by tye (Sage) on May 12, 2007 at 06:05 UTC | |
by runrig (Abbot) on Jul 12, 2007 at 21:38 UTC | |
|
Re: RFC: DBIx::Iterator
by chromatic (Archbishop) on May 12, 2007 at 04:30 UTC | |
by tye (Sage) on May 12, 2007 at 05:29 UTC | |
by chromatic (Archbishop) on May 12, 2007 at 06:16 UTC | |
by runrig (Abbot) on May 12, 2007 at 04:49 UTC | |
by chromatic (Archbishop) on May 12, 2007 at 06:09 UTC | |
by runrig (Abbot) on May 12, 2007 at 16:16 UTC | |
by chromatic (Archbishop) on May 12, 2007 at 23:21 UTC | |
by runrig (Abbot) on May 18, 2007 at 18:33 UTC |