The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/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<Genezzo::PushHash::hph>) 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<Genezzo::Dict>

=item dbh
(Required) - database handle object from B<Genezzo::GenDBI>

=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<perl(1)>.

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<http://www.genezzo.com>

=cut