#!/usr/bin/perl # # $Header: /Users/claude/fuzz/lib/Genezzo/Row/RCS/RSJoinA.pm,v 1.8 2006/10/26 07:24:28 claude Exp claude $ # # copyright (c) 2005,2006 Jeffrey I Cohen, all rights reserved, worldwide # # use strict; use warnings; package Genezzo::Row::RSJoinA; use Genezzo::Util; use Genezzo::PushHash::PushHash; use Carp; use warnings::register; our @ISA = "Genezzo::PushHash::PushHash" ; our $GZERR = sub { my %args = (@_); return unless (exists($args{msg})); if (exists($args{self})) { my $self = $args{self}; if (defined($self) && exists($self->{GZERR})) { my $err_cb = $self->{GZERR}; return &$err_cb(%args); } } my $warn = 0; if (exists($args{severity})) { my $sev = uc($args{severity}); $sev = 'WARNING' if ($sev =~ m/warn/i); # don't print 'INFO' prefix if ($args{severity} !~ m/info/i) { printf ("%s: ", $sev); $warn = 1; } } # XXX XXX XXX print __PACKAGE__, ": ", $args{msg}; # print $args{msg}; # carp $args{msg} # if (warnings::enabled() && $warn); }; sub _init { # whoami; # greet @_; my $self = shift; my %required = ( rs_list => "no rowsource list!", dict => "no dictionary!", magic_dbh => "no dbh!" ); my %args = (@_); return 0 unless (Validate(\%args, \%required)); $self->{rs_list} = $args{rs_list}; $self->{dict} = $args{dict}; $self->{dbh} = $args{magic_dbh}; if (defined($args{select_list})) { # greet $args{select_list}; $self->{select_list} = $args{select_list}; return 0 unless (defined($args{alias_list})); $self->{alias_list} = $args{alias_list}; } # XXX XXX XXX XXX: why doesn't this work? # need to build a composite rid if joining multiple row sources $self->{rid_fixup} = (scalar(@{$self->{rs_list}}) > 1); return 1; } sub TIEHASH { #sub new # greet @_; # whoami; my $invocant = shift; my $class = ref($invocant) || $invocant ; # my $self = $class->SUPER::TIEHASH(@_); my $self = {}; my %args = (@_); return undef unless (_init($self,%args)); if ((exists($args{GZERR})) && (defined($args{GZERR})) && (length($args{GZERR}))) { # NOTE: don't supply our GZERR here - will get # recursive failure... $self->{GZERR} = $args{GZERR}; } return bless $self, $class; } # end new sub SelectList { # whoami; my $self = shift; # return undef; # XXX XXX XXX XXX XXX XXX return $self->{select_list} if (exists($self->{select_list})); return undef; } # HPush public method (not part of standard hash) sub HPush { my $self = shift; my $rs = $self->{rs_list}; # whoami; return ($rs->HPush(@_)); } sub HCount { my $self = shift; my $rsl = $self->{rs_list}; whoami; return 0 # terminate if no row sources unless (scalar(@{$rsl})); # multiply the counts (cartesian product) my $grandtotal = 1; # multiplicative identity for first row source for my $rs (@{$rsl}) { $grandtotal *= $rs->HCount(@_); return 0 # terminate if one row source is empty... unless ($grandtotal); } return $grandtotal; } # standard hash methods follow sub STORE { my $self = shift; my $rs = $self->{rs_list}; whoami; return ($rs->STORE(@_)); } sub FETCH { my ($self, $place) = @_; return $self->_localFetch($place, "STANDARD"); } sub _localFetch { my ($self, $place, $mode) = @_; my $rsl = $self->{rs_list}; # whoami; my @placelist; if ($self->{rid_fixup}) { # URL-style substitution to handle spaces, weird chars $place =~ s/\%([A-Fa-f0-9]{2})/pack('C', hex($1))/seg; @placelist = UnPackRow($place, $Genezzo::Util::UNPACK_TEMPL_ARR); # } else { push @placelist, $place; } if ($mode eq "STANDARD") { my @outval; if (scalar(@{$rsl} == 1)) { my $keyval = shift @placelist; # NOTE: each rowsource must have at least one row for a valid join return undef unless (defined($keyval)); return $rsl->[0]->FETCH($keyval); } for my $rs (@{$rsl}) { my $keyval = shift @placelist; # NOTE: each rowsource must have at least one row for a valid join return undef unless (defined($keyval)); push @outval, @{$rs->FETCH($keyval)}; } return (\@outval); } elsif ($mode eq "HASH") { my $outhsh = {}; my $idx = 0; for my $rs (@{$rsl}) { my $keyval = shift @placelist; # NOTE: each rowsource must have at least one row for a valid join return undef unless (defined($keyval)); my $alias = $self->{alias_list}->[$idx]; $outhsh->{$alias} = $rs->FETCH($keyval); $idx++; } return $outhsh; } return undef; } sub FIRSTKEY { my $self = shift; my $rsl = $self->{rs_list}; # whoami; my @firstkey; for my $rs (@{$rsl}) { my $keyval = $rs->FIRSTKEY(@_); # NOTE: each rowsource must have at least one row for a valid join return undef unless (defined($keyval)); push @firstkey, $keyval; } if ($self->{rid_fixup}) { # create a composite key out of all the firstkeys my $packstr = PackRow(\@firstkey); # URL-style substitution to handle spaces, weird chars $packstr =~ s/([^a-zA-Z0-9])/uc(sprintf("%%%02lx", ord $1))/eg; return ($packstr); } # just a single rowsource, return rid return $firstkey[0]; } sub NEXTKEY { my ($self, $prevkey) = @_; my $rsl = $self->{rs_list}; # whoami; return (undef) unless (defined ($prevkey)); my @prevkeylist; if ($self->{rid_fixup}) { # URL-style substitution to handle spaces, weird chars $prevkey =~ s/\%([A-Fa-f0-9]{2})/pack('C', hex($1))/seg; @prevkeylist = UnPackRow($prevkey, $Genezzo::Util::UNPACK_TEMPL_ARR); # } else { push @prevkeylist, $prevkey; } my $idx = scalar(@prevkeylist) - 1; while ($idx >= 0) { # starting at the last rowsource in the list, get the nextkey my $nextkey = $rsl->[$idx]->NEXTKEY($prevkeylist[$idx]); if (defined($nextkey)) { # got it - update that portion of the composite key $prevkeylist[$idx] = $nextkey; # advanced trailing key portion - exit the loop and return # updated key value last; } else { # if rowsource at idx=0 is lastkey, then there is no NEXTKEY return undef unless ($idx > 0); # reset this portion of the key to its firstkey, then # decrement the index in order to advance the prior # segment of the key $nextkey = $rsl->[$idx]->FIRSTKEY(); # NOTE: each rowsource must have at least one row for a valid join return undef unless (defined($nextkey)); $prevkeylist[$idx] = $nextkey; # not done yet -- get the nextkey for the prior portion } $idx--; } # end while return undef unless ($idx >= 0); if ($self->{rid_fixup}) { my $packstr = PackRow(\@prevkeylist); # URL-style substitution to handle spaces, weird chars $packstr =~ s/([^a-zA-Z0-9])/uc(sprintf("%%%02lx", ord $1))/eg; return ($packstr); } # just a single rowsource, return rid return $prevkeylist[0]; } sub EXISTS { my $self = shift; my $rs = $self->{rs_list}; # whoami; return ($rs->EXISTS(@_)); } sub DELETE { my $self = shift; my $rs = $self->{rs_list}; # whoami; return ($rs->DELETE(@_)); } sub CLEAR { my $self = shift; my $rs = $self->{rs_list}; # whoami; return ($rs->CLEAR(@_)); } sub AUTOLOAD { my $self = shift; my $rsl = $self->{rs_list}; our $AUTOLOAD; my $newfunc = $AUTOLOAD; $newfunc =~ s/.*:://; return if $newfunc eq 'DESTROY'; # greet $newfunc; if (scalar(@{$rsl}) == 1) { # handle FIRSTCOUNT, etc, for case of single row source return ($rsl->[0]->$newfunc(@_)); } return ($rsl->$newfunc(@_)); } sub SQLPrepare # get a DBI-style statement handle { my $self = shift; my %args = @_; $args{pushhash} = $self; $args{rs_list} = $self->{rs_list}; $args{dict} = $self->{dict}; $args{magic_dbh} = $self->{dbh}; if (defined($self->{select_list})) { $args{select_list} = $self->{select_list}; } $args{use_select_list} = defined($self->SelectList()); if ((exists($self->{GZERR})) && (defined($self->{GZERR}))) { $args{GZERR} = $self->{GZERR}; } my $sth = Genezzo::Row::SQL_RSJoinA->new(%args); return $sth; } package Genezzo::Row::SQL_RSJoinA; use strict; use warnings; use Genezzo::Util; sub _init { my $self = shift; my %args = (@_); return 0 unless (defined($args{pushhash})); $self->{pushhash} = $args{pushhash}; $self->{dict} = $args{dict}; $self->{dbh} = $args{magic_dbh}; return 0 unless (defined($args{rs_list})); my $rsl = $args{rs_list}; $self->{sql_rs} = []; for my $rs (@{$rsl}) { my $prep = $rs->SQLPrepare(@_); return 0 unless (defined($prep)); push @{$self->{sql_rs}}, $prep; } if (defined($args{select_list})) { # greet $args{select_list}; $self->{select_list} = $args{select_list}; } $self->{rownum} = 0; $self->{use_select_list} = $args{use_select_list}; if (defined($args{filter})) { $self->{SQLFilter} = $args{filter}; } return 1; } sub new { # whoami; my $invocant = shift; my $class = ref($invocant) || $invocant ; my $self = { }; my %args = (@_); if ((exists($args{GZERR})) && (defined($args{GZERR})) && (length($args{GZERR}))) { # NOTE: don't supply our GZERR here - will get # recursive failure... $self->{GZERR} = $args{GZERR}; } return undef unless (_init($self,%args)); return bless $self, $class; } # end new # SQL-style execute and fetch functions sub SQLExecute { my $self = shift; my $sql_rsl = $self->{sql_rs}; my $newlist = []; for my $rs (@{$sql_rsl}) { my $prep = $rs->SQLExecute(@_); return 0 unless (defined($prep)); push @{$newlist}, $prep; } $self->{sql_rs} = $newlist; $self->{SQLFetchKey} = $self->{pushhash}->FIRSTKEY(); return (1); } sub SQLFetch { my $self = shift; my $rsl = $self->{sql_rs}; my $is_undef; my $fullfilter = $self->{SQLFilter}; my $filter = (defined($fullfilter)) ? $fullfilter->{filter} : undef; # whoami; my $tc_rownum = $self->{rownum} + 1; my $tc_dict = $self->{dict}; my $tc_dbh = $self->{dbh}; # my ($tc_rid, $vv) = $rs->SQLFetch(@_); my ($rid, $vv); L_w1: while (defined($self->{SQLFetchKey})) { my $currkey = $self->{SQLFetchKey}; my $outarr = $self->{pushhash}->_localFetch($currkey, "HASH"); my $get_alias_col = $outarr; # save the value of the key because we pre-advance to the next one $self->{SQLFetchKey} = $self->{pushhash}->NEXTKEY($currkey); $rid = $currkey; $vv = $outarr; greet $rid, $vv; return undef # check if child has terminated unless (defined($rid)); if (!(defined($vv) && defined($filter))) { last L_w1; } else { # filter is defined my $val; # be very paranoid - filter might be invalid perl eval {$val = &$filter($self, $currkey, $outarr, $get_alias_col, $tc_rownum) }; if ($@) { whisper "filter blew up: $@"; greet $fullfilter; my $msg = "bad filter: $@\n" ; # $msg .= Dumper($fullfilter) # if (defined($fullfilter)); my %earg = (self => $self, msg => $msg, severity => 'warn'); &$GZERR(%earg) if (defined($GZERR)); return undef; } last L_w1 unless (!$val); # clear out rid and values in case next fetch hits EOF $rid = undef; $vv = undef; } } # end while my @big_arr; if (defined($vv)) { if ($self->{use_select_list}) { my $outarr = $vv; my $get_alias_col = $outarr; for my $valex (@{$self->{select_list}}) { unless (defined($valex->{value_expression})) { my $msg = "no value expression!"; my %earg = (self => $self, msg => $msg, severity => 'warn'); &$GZERR(%earg) if (defined($GZERR)); return undef; } if (defined($valex->{value_expression}->{vx})) { $is_undef = 0; } else { $is_undef = 1; # NOTE: undefined value expression only legal for # TFN literal unless (exists($valex->{value_expression}->{tfn_literal})) { my $msg = "no value expression vx!"; my %earg = (self => $self, msg => $msg, severity => 'warn'); &$GZERR(%earg) if (defined($GZERR)); return undef; } } my $vx_val; my $v_str; $v_str = '$vx_val = ' . $valex->{value_expression}->{vx} . ';' unless ($is_undef); # whoami $v_str; { my $msg = ""; my $status; if ($is_undef) { # just set the vx_val to return an undef $vx_val = undef; $status = 1; } else { $status = eval "$v_str"; } unless (defined($status)) { # $@ must be non-null if eval failed $msg .= $@ if $@; } # NOTE: status of undef is ok if no warning message if (defined($status) || !(length($msg))) { push @big_arr, $vx_val; } else { # warn $@ if $@; $msg .= "\nbad value expression:\n"; $msg .= $valex->{value_expression}->{vx} . "\n"; my %earg = (self => $self, msg => $msg, severity => 'warn'); &$GZERR(%earg) if (defined($GZERR)); greet $outarr; return undef; } } } # end for all valex } else { push @big_arr, @{$vv}; } $self->{rownum} += 1; } # return ($tc_rid, \@big_arr); return ($rid, \@big_arr); } sub AUTOLOAD { my $self = shift; my $rs = $self->{sql_rs}; our $AUTOLOAD; my $newfunc = $AUTOLOAD; $newfunc =~ s/.*:://; return if $newfunc eq 'DESTROY'; # greet $newfunc; return ($rs->$newfunc(@_)); } END { } 1; __END__ # Below is stub documentation for your module. You better edit it! =head1 NAME Genezzo::Row::RSJoinA - Row Source Join [A] =head1 SYNOPSIS use Genezzo::Row::RSJoinA; # see Genezzo::GenDBI usage =head1 DESCRIPTION RSJoinA is a hierarchical pushhash (see L) class which performs a cartesian product of multiple rowsources. =head1 ARGUMENTS =over 4 =item row source list (Required) - list of row sources to join =item dict (Required) - dictionary object from B =item dbh (Required) - database handle object from B =back =head1 FUNCTIONS RSJoinA supports all standard READ-ONLY hph hierarchical pushhash operations, like FETCH, FIRSTKEY, NEXTKEY, HCOUNT =head2 EXPORT =head1 LIMITATIONS HPUSH, STORE, EXISTS, DELETE, CLEAR are probably broken... =head1 TODO =over 4 =item build nested-loop, sort-merge, hash join =back =head1 AUTHOR Jeffrey I. Cohen, jcohen@genezzo.com =head1 SEE ALSO L. Copyright (c) 2005, 2006 Jeffrey I Cohen. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Address bug reports and comments to: jcohen@genezzo.com For more information, please visit the Genezzo homepage at L =cut