Author: timbo
Date: Mon Jan 29 05:21:16 2007
New Revision: 8745

Added:
   dbi/trunk/lib/DBD/Gofer/
   dbi/trunk/lib/DBD/Gofer.pm
   dbi/trunk/lib/DBD/Gofer/Transport/
   dbi/trunk/lib/DBD/Gofer/Transport/Base.pm
   dbi/trunk/lib/DBD/Gofer/Transport/null.pm
   dbi/trunk/lib/DBI/Gofer/
   dbi/trunk/lib/DBI/Gofer/Execute.pm
   dbi/trunk/lib/DBI/Gofer/Request.pm
   dbi/trunk/lib/DBI/Gofer/Response.pm
   dbi/trunk/lib/DBI/Gofer/Transport/
   dbi/trunk/lib/DBI/Gofer/Transport/Base.pm
   dbi/trunk/t/85gofer.t
Removed:
   dbi/trunk/lib/DBD/Forward/
   dbi/trunk/lib/DBD/Forward.pm
   dbi/trunk/lib/DBI/Forward/
   dbi/trunk/t/85forward.t
Modified:
   dbi/trunk/MANIFEST

Log:
Delete DBD::Forward and add DBD::Gofer (with many changes)


Modified: dbi/trunk/MANIFEST
==============================================================================
--- dbi/trunk/MANIFEST  (original)
+++ dbi/trunk/MANIFEST  Mon Jan 29 05:21:16 2007
@@ -24,9 +24,9 @@
 lib/DBD/DBM.pm                 A driver for DBM files (uses DBD::File)
 lib/DBD/ExampleP.pm            A very simple example Driver module
 lib/DBD/File.pm                        A driver base class for simple drivers
-lib/DBD/Forward.pm              DBD::Forward 'stateless proxy' driver
-lib/DBD/Forward/Transport/Base.pm Base class for DBD::Forward driver transport 
classes
-lib/DBD/Forward/Transport/null.pm DBD::Forward transport that executes locally 
(for testing)
+lib/DBD/Gofer.pm                DBD::Gofer 'stateless proxy' driver
+lib/DBD/Gofer/Transport/Base.pm Base class for DBD::Gofer driver transport 
classes
+lib/DBD/Gofer/Transport/null.pm DBD::Gofer transport that executes locally 
(for testing)
 lib/DBD/NullP.pm               An empty example Driver module
 lib/DBD/Proxy.pm               Proxy driver
 lib/DBD/Sponge.pm              A driver for fake cursors (precached data)
@@ -37,10 +37,10 @@
 lib/DBI/DBD.pm                 Some basic help for people writing DBI drivers
 lib/DBI/DBD/Metadata.pm                Metadata tools for people writing DBI 
drivers
 lib/DBI/FAQ.pm                 The DBI FAQ in module form for perldoc
-lib/DBI/Forward/Execute.pm      Execution logic for DBD::Forward server
-lib/DBI/Forward/Request.pm      Request object from DBD::Forward
-lib/DBI/Forward/Response.pm     Response object for DBD::Forward
-lib/DBI/Forward/Transport/Base.pm Base class for DBD::Forward server transport 
classes
+lib/DBI/Gofer/Execute.pm        Execution logic for DBD::Gofer server
+lib/DBI/Gofer/Request.pm        Request object from DBD::Gofer
+lib/DBI/Gofer/Response.pm       Response object for DBD::Gofer
+lib/DBI/Gofer/Transport/Base.pm Base class for DBD::Gofer server transport 
classes
 lib/DBI/Profile.pm             Manage DBI usage profile data
 lib/DBI/ProfileData.pm
 lib/DBI/ProfileDumper.pm
@@ -79,7 +79,7 @@
 t/70callbacks.t
 t/72childhandles.t
 t/80proxy.t
-t/85forward.t
+t/85gofer.t
 t/pod.t
 test.pl                        Assorted informal tests, including tests for 
memory leaks
 typemap

Added: dbi/trunk/lib/DBD/Gofer.pm
==============================================================================
--- (empty file)
+++ dbi/trunk/lib/DBD/Gofer.pm  Mon Jan 29 05:21:16 2007
@@ -0,0 +1,652 @@
+{
+    package DBD::Gofer;
+
+    use strict;
+
+    require DBI;
+    require DBI::Gofer::Request;
+    require DBI::Gofer::Response;
+    require Carp;
+
+    our $VERSION = sprintf("0.%06d", q$Revision: 8705 $ =~ /(\d+)/o);
+
+#   $Id: Gofer.pm 8705 2007-01-26 01:08:25Z timbo $
+#
+#   Copyright (c) 2007, Tim Bunce, Ireland
+#
+#   You may distribute under the terms of either the GNU General Public
+#   License or the Artistic License, as specified in the Perl README file.
+
+
+
+    # attributes we'll allow local STORE
+    our %xxh_local_store_attrib = map { $_=>1 } qw(
+        Active
+        CachedKids
+        Callbacks
+        ErrCount Executed
+        FetchHashKeyName
+        HandleError HandleSetErr
+        InactiveDestroy
+        PrintError PrintWarn
+        Profile
+        RaiseError
+        RootClass
+        ShowErrorStatement
+        Taint TaintIn TaintOut
+        TraceLevel
+        Warn
+        dbi_quote_identifier_cache
+        dbi_connect_closure
+        dbi_go_execute_unique
+    );
+    our %xxh_local_store_if_same_attrib = map { $_=>1 } qw(
+        Username
+        dbi_connect_method
+    );
+
+    our $drh = undef;  # holds driver handle once initialised
+    our $methods_already_installed;
+
+    sub driver{
+       return $drh if $drh;
+
+        DBI->setup_driver('DBD::Gofer');
+
+        unless ($methods_already_installed++) {
+            DBD::Gofer::db->install_method('go_dbh_method', { O=> 0x0004 }); # 
IMA_KEEP_ERR
+            DBD::Gofer::st->install_method('go_sth_method', { O=> 0x0004 }); # 
IMA_KEEP_ERR
+        }
+
+       my($class, $attr) = @_;
+       $class .= "::dr";
+       ($drh) = DBI::_new_drh($class, {
+           'Name' => 'Gofer',
+           'Version' => $VERSION,
+           'Attribution' => 'DBD Gofer by Tim Bunce',
+        });
+
+       $drh;
+    }
+
+    sub CLONE {
+        undef $drh;
+    }
+
+}
+
+
+{   package DBD::Gofer::dr; # ====== DRIVER ======
+
+    my %dsn_attr_defaults = (
+        go_dsn => undef,
+        go_url => undef,
+        go_transport => undef,
+    );
+
+    $imp_data_size = 0;
+    use strict;
+
+    sub connect {
+        my($drh, $dsn, $user, $auth, $attr)= @_;
+        my $orig_dsn = $dsn;
+
+        # first remove dsn= and everything after it
+        my $go_dsn = ($dsn =~ s/\bdsn=(.*)$// && $1)
+            or return $drh->set_err(1, "No dsn= argument in '$orig_dsn'");
+
+        my %dsn_attr = (%dsn_attr_defaults, go_dsn => $go_dsn);
+        # extract any go_ attributes from the connect() attr arg
+        for my $k (grep { /^go_/ } keys %$attr) {
+            $dsn_attr{$k} = delete $attr->{$k};
+        }
+        # then override those with any attributes embedded in our dsn (not 
go_dsn)
+        for my $kv (grep /=/, split /;/, $dsn, -1) {
+            my ($k, $v) = split /=/, $kv, 2;
+            $dsn_attr{ "go_$k" } = $v;
+        }
+        if (keys %dsn_attr > keys %dsn_attr_defaults) {
+            delete @dsn_attr{ keys %dsn_attr_defaults };
+            return $drh->set_err(1, "Unknown attributes: @{[ keys %dsn_attr 
]}");
+        }
+
+        my $transport_class = $dsn_attr{go_transport}
+            or return $drh->set_err(1, "No transport= argument in 
'$orig_dsn'");
+        $transport_class = "DBD::Gofer::Transport::$dsn_attr{go_transport}"
+            unless $transport_class =~ /::/;
+        eval "require $transport_class"
+            or return $drh->set_err(1, "Error loading $transport_class: $@");
+        my $go_trans = eval { $transport_class->new(\%dsn_attr) }
+            or return $drh->set_err(1, "Error instanciating $transport_class: 
$@");
+
+        # XXX user/pass of fwd server vs db server
+        my $request_class = "DBI::Gofer::Request";
+        my $go_request = eval {
+            # copy and delete any attributes we can't serialize (and don't 
want to)
+            my $go_attr = { %$attr };
+            delete @{$go_attr}{qw(Profile HandleError HandleSetErr Callbacks)};
+            $request_class->new({
+                connect_args => [ $go_dsn, $user, $auth, $go_attr ]
+            })
+        } or return $drh->set_err(1, "Error instanciating $request_class $@");
+
+        my ($dbh, $dbh_inner) = DBI::_new_dbh($drh, {
+            'Name' => $dsn,
+            'USER' => $user,
+            go_trans => $go_trans,
+            go_request => $go_request,
+            go_policy => undef, # XXX
+        });
+
+        $dbh->STORE(Active => 0); # mark as inactive temporarily for STORE
+
+        # test the connection XXX control via a policy later
+        unless ($dbh->go_dbh_method('ping', undef)) {
+            return undef if $dbh->err; # error already recorded, typically
+            return $dbh->set_err(1, "ping failed");
+        }
+            # unless $policy->skip_connect_ping($attr, $dsn, $user, $auth, 
$attr);
+
+        # Active not set until connected() called.
+
+        return $dbh;
+    }
+
+    sub DESTROY { undef }
+}
+
+
+{   package DBD::Gofer::db; # ====== DATABASE ======
+    $imp_data_size = 0;
+    use strict;
+    use Carp qw(croak);
+
+    my %dbh_local_store_attrib = %DBD::Gofer::xxh_local_store_attrib;
+
+    sub connected {
+        shift->STORE(Active => 1);
+    }
+
+    sub go_dbh_method {
+        my ($dbh, $method, $meta, @args) = @_;
+        my $request = $dbh->{go_request};
+        $request->init_request($method, [EMAIL PROTECTED], wantarray);
+
+        my $transport = $dbh->{go_trans}
+            or return $dbh->set_err(1, "Not connected (no transport)");
+
+        eval { $transport->transmit_request($request) }
+            or return $dbh->set_err(1, "transmit_request failed: $@");
+
+        my $response = $transport->receive_response;
+        my $rv = $response->rv;
+
+        $dbh->{go_response} = $response;
+
+        if (my $resultset_list = $response->sth_resultsets) {
+            # setup an sth but don't execute/forward it
+            my $sth = $dbh->prepare(undef, { go_skip_early_prepare => 1 }); # 
XXX
+            # set the sth response to our dbh response
+            (tied %$sth)->{go_response} = $response;
+            # setup the set with the results in our response
+            $sth->more_results;
+            $rv = [ $sth ];
+        }
+
+        $dbh->set_err($response->err, $response->errstr, $response->state);
+
+        return (wantarray) ? @$rv : $rv->[0];
+    }
+
+    # Methods that should be forwarded
+    # XXX get_info? special sub to lazy-cache individual values
+    for my $method (qw(
+        data_sources
+        table_info column_info primary_key_info foreign_key_info 
statistics_info
+        type_info_all get_info
+        parse_trace_flags parse_trace_flag
+        func
+    )) {
+        no strict 'refs';
+        *$method = sub { return shift->go_dbh_method($method, undef, @_) }
+    }
+
+    # Methods that should always fail
+    for my $method (qw(
+        begin_work commit rollback
+    )) {
+        no strict 'refs';
+        *$method = sub { return shift->set_err(1, "$method not available with 
DBD::Gofer") }
+    }
+
+    # for quote we rely on the default method + type_info_all
+    # for quote_identifier we rely on the default method + get_info
+
+    sub do {
+        my $dbh = shift;
+        delete $dbh->{Statement}; # avoid "Modification of non-creatable hash 
value attempted"
+        $dbh->{Statement} = $_[0]; # for profiling and ShowErrorStatement
+        return $dbh->go_dbh_method('do', undef, @_);
+    }
+
+    sub ping {
+        my $dbh = shift;
+        return $dbh->set_err(0, "can't ping while not connected") # warning
+            unless $dbh->SUPER::FETCH('Active');
+        # XXX local or remote - add policy attribute
+        return $dbh->go_dbh_method('ping', undef, @_);
+    }
+
+    sub last_insert_id {
+        my $dbh = shift;
+        my $response = $dbh->{go_response} or return undef;
+        # will be undef unless last_insert_id was explicitly requested
+        return $response->last_insert_id;
+    }
+
+    sub FETCH {
+       my ($dbh, $attrib) = @_;
+
+        # forward driver-private attributes
+        if ($attrib =~ m/^[a-z]/) { # XXX policy? precache on connect?
+            my $value = $dbh->go_dbh_method('FETCH', undef, $attrib);
+            $dbh->{$attrib} = $value;
+            return $value;
+        }
+
+       # else pass up to DBI to handle
+       return $dbh->SUPER::FETCH($attrib);
+    }
+
+    sub STORE {
+       my ($dbh, $attrib, $value) = @_;
+        if ($attrib eq 'AutoCommit') {
+            return $dbh->SUPER::STORE($attrib => -901) if $value;
+            croak "Can't enable transactions when using DBD::Gofer";
+        }
+       return $dbh->SUPER::STORE($attrib => $value)
+            # we handle this attribute locally
+            if $dbh_local_store_attrib{$attrib}
+            # not yet connected (and being called by connect())
+            or not $dbh->FETCH('Active');
+
+       return $dbh->SUPER::STORE($attrib => $value)
+            if $DBD::Gofer::xxh_local_store_if_same_attrib{$attrib}
+            && do { local $^W; $value eq $dbh->FETCH($attrib) }; # XXX undefs
+
+        # dbh attributes are set at connect-time - see connect()
+        Carp::carp("Can't alter \$dbh->{$attrib}");
+        return $dbh->set_err(1, "Can't alter \$dbh->{$attrib}");
+    }
+
+    sub disconnect {
+       my $dbh = shift;
+        $dbh->{go_trans} = undef;
+       $dbh->STORE(Active => 0);
+    }
+
+    # XXX + prepare_cached ?
+    #
+    sub prepare {
+       my ($dbh, $statement, $attr)= @_;
+
+        return $dbh->set_err(1, "Can't prepare when disconnected")
+            unless $dbh->FETCH('Active');
+
+        my $policy = $attr->{go_policy} || $dbh->{go_policy};
+
+       my ($sth, $sth_inner) = DBI::_new_sth($dbh, {
+           Statement => $statement,
+            go_prepare_call => [ 'prepare', [ $statement, $attr ] ],
+            go_method_calls => [],
+            go_request => $dbh->{go_request},
+            go_trans => $dbh->{go_trans},
+            go_policy => $policy,
+        });
+
+        #my $p_sep = $policy->skip_early_prepare($attr, $dbh, $statement, 
$attr, $sth);
+        my $p_sep = 0;
+
+        $p_sep = 1 if not defined $statement; # XXX hack, see go_dbh_method
+        if (not $p_sep) {
+            $sth->go_sth_method() or return undef;
+        }
+
+       return $sth;
+    }
+
+}
+
+
+{   package DBD::Gofer::st; # ====== STATEMENT ======
+    $imp_data_size = 0;
+    use strict;
+
+    my %sth_local_store_attrib = (%DBD::Gofer::xxh_local_store_attrib, 
NUM_OF_FIELDS => 1);
+
+    sub go_sth_method {
+        my ($sth) = @_;
+
+        if (my $ParamValues = $sth->{ParamValues}) {
+            my $ParamAttr = $sth->{ParamAttr};
+            while ( my ($p, $v) = each %$ParamValues) {
+                # unshift to put binds before execute call
+                unshift @{ $sth->{go_method_calls} },
+                    [ 'bind_param', $p, $v, $ParamAttr->{$p} ];
+            }
+        }
+
+        my $request = $sth->{go_request};
+        $request->init_request(@{$sth->{go_prepare_call}}, undef);
+        $request->sth_method_calls($sth->{go_method_calls});
+        $request->sth_result_attr({});
+
+        my $transport = $sth->{go_trans}
+            or return $sth->set_err(1, "Not connected (no transport)");
+        eval { $transport->transmit_request($request) }
+            or return $sth->set_err(1, "transmit_request failed: $@");
+        my $response = $transport->receive_response;
+        $sth->{go_response} = $response;
+        delete $sth->{go_method_calls};
+
+        if ($response->sth_resultsets) {
+            # setup first resultset - including atributes
+            $sth->more_results;
+        }
+        else {
+            $sth->{go_rows} = $response->rv;
+        }
+        # set error/warn/info (after more_results as that'll clear err)
+        $sth->set_err($response->err, $response->errstr, $response->state);
+
+        return $response->rv;
+    }
+
+
+    # sth methods that should always fail, at least for now
+    for my $method (qw(
+        bind_param_inout bind_param_array bind_param_inout_array execute_array 
execute_for_fetch
+    )) {
+        no strict 'refs';
+        *$method = sub { return shift->set_err(1, "$method not available with 
DBD::Gofer, yet (patches welcome)") }
+    }
+
+
+    sub bind_param {
+        my ($sth, $param, $value, $attr) = @_;
+        $sth->{ParamValues}{$param} = $value;
+        $sth->{ParamAttr}{$param} = $attr;
+        return 1;
+    }
+
+
+    sub execute {
+       my $sth = shift;
+        $sth->bind_param($_, $_[$_-1]) for ([EMAIL PROTECTED]);
+        push @{ $sth->{go_method_calls} }, [ 'execute' ];
+        return $sth->go_sth_method;
+    }
+
+
+    sub more_results {
+       my ($sth) = @_;
+
+       $sth->finish if $sth->FETCH('Active');
+
+       my $resultset_list = $sth->{go_response}->sth_resultsets
+            or return $sth->set_err(1, "No sth_resultsets");
+
+        my $meta = shift @$resultset_list
+            or return undef; # no more result sets
+
+        # pull out the special non-atributes first
+        my ($rowset, $err, $errstr, $state)
+            = delete @{$meta}{qw(rowset err errstr state)};
+
+        # copy meta attributes into attribute cache
+        my $NUM_OF_FIELDS = delete $meta->{NUM_OF_FIELDS};
+        $sth->STORE('NUM_OF_FIELDS', $NUM_OF_FIELDS);
+        $sth->{$_} = $meta->{$_} for keys %$meta;
+
+        if (($NUM_OF_FIELDS||0) > 0) {
+            $sth->{go_rows}           = ($rowset) ? @$rowset : -1;
+            $sth->{go_current_rowset} = $rowset;
+            $sth->{go_current_rowset_err} = [ $err, $errstr, $state ]
+                if defined $err;
+            $sth->STORE(Active => 1) if $rowset;
+        }
+
+       return $sth;
+    }
+
+
+    sub fetchrow_arrayref {
+       my ($sth) = @_;
+       my $resultset = $sth->{go_current_rowset}
+            or return $sth->set_err( @{ $sth->{go_current_rowset_err} } );
+        return $sth->_set_fbav(shift @$resultset) if @$resultset;
+       $sth->finish;     # no more data so finish
+       return undef;
+    }
+    *fetch = \&fetchrow_arrayref; # alias
+
+
+    sub fetchall_arrayref {
+        my ($sth, $slice, $max_rows) = @_;
+        my $mode = ref($slice) || 'ARRAY';
+        return $sth->SUPER::fetchall_arrayref($slice, $max_rows)
+            if ref($slice) or defined $max_rows;
+       my $resultset = $sth->{go_current_rowset}
+            or return $sth->set_err( @{ $sth->{go_current_rowset_err} } );
+       $sth->finish;     # no more data so finish
+        return $resultset;
+    }
+
+
+    sub rows {
+        return shift->{go_rows};
+    }
+
+
+    sub STORE {
+       my ($sth, $attrib, $value) = @_;
+       return $sth->SUPER::STORE($attrib => $value)
+            if $sth_local_store_attrib{$attrib}  # handle locally
+            or $attrib =~ m/^[a-z]/;             # driver-private
+
+        # XXX could perhaps do
+        # XXX? push @{ $sth->{go_method_calls} }, [ 'STORE', $attrib, $value ];
+        Carp::carp("Can't alter \$sth->{$attrib}");
+        return $sth->set_err(1, "Can't alter \$sth->{$attrib}");
+    }
+
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+DBD::Gofer - A stateless-proxy driver for communicating with a remote DBI
+
+=head1 SYNOPSIS
+
+  use DBI;
+
+  $dbh = DBI->connect("dbi:Gofer:transport=$transport;...;dsn=$dsn",
+                      $user, $passwd, \%attributes);
+
+The C<transport=$transport> part specifies the name of the module to use to
+transport the requests to the remote DBI. If $transport doesn't contain any
+double colons then it's prefixed with C<DBD::Gofer::Transport::>.
+
+The C<dsn=$dsn> part I<must> be the last element of the dsn because everything
+after C<dsn=> is assumed to be the DSN that the remote DBI should use.
+
+The C<...> represents attributes that influence the operation of the driver or
+transport. These are described below or in the documentation of the transport
+module being used.
+
+=head1 DESCRIPTION
+
+DBD::Gofer is a DBI database driver that forwards requests to another DBI 
driver,
+usually in a seperate process, often on a separate machine.
+
+It is very similar to DBD::Proxy. The major difference is that DBD::Gofer
+assumes no state is maintained on the remote end. What does that mean?
+It means that every request contains all the information needed to create the
+required state. (So, for example, every request includes the DSN to connect 
to.)
+Each request can be sent to any available server. The server executes
+the request and returns a single response that includes all the data.
+
+This is very similar to the way http works as a stateless protocol for the web.
+Each request from your web browser can be handled by a different web server 
process.
+
+This may seem like pointless overhead but there are situations where this is a
+very good thing. Let's consider a specific case.
+
+Imagine using DBD::Gofer with an http transport. Your application calls
+connect(), prepare("select * from table where foo=?"), bind_param(), and 
execute().
+At this point DBD::Gofer builds a request containing all the information
+about the method calls. It then uses the httpd transport to send that request
+to an apache web server.
+
+This 'dbi execute' web server executes the request (using DBI::Gofer::Execute
+and related modules) and builds a response that contains all the rows of data,
+if the statement returned any, along with all the attributes that describe the
+results, such as $sth->{NAME}. This response is sent back to DBD::Gofer which
+unpacks it and presents it to the application as if it had executed the
+statement itself.
+
+Okay, but you still don't see the point? Well let's consider what we've gained:
+
+=head3 Connection Pooling and Throttling
+
+The 'dbi execute' web server leverages all the functionality of web
+infrastructure in terms of load balancing, high-availability, firewalls, access
+management, proxying, caching.
+
+At it's most basic level you get a configurable pool of persistent database 
connections.
+
+=head3 Simple Scaling
+
+Got thousands of processes all trying to connect to the database? You can use
+DBD::Gofer to connect them to your pool of 'dbi execute' web servers instead.
+
+=head3 Caching
+
+Not yet implemented, but the single request-response architecture lends itself 
to caching.
+
+=head3 Fewer Network Round-trips
+
+DBD::Gofer sends as few requests as possible.
+
+=head3 Thin Clients / Unsupported Platforms
+
+You no longer need drivers for your database on every system.
+DBD::Gofer is pure perl
+
+=head1 CONSTRAINTS
+
+There are naturally a some constraints imposed by DBD::Gofer. But not many:
+
+=head2 You can't change database handle attributes
+
+You can't change database handle attributes after you've connected.
+Use the connect() call to specify all the attribute settings you want.
+
+This is because it's critical that when a request is complete the database
+handle is left in the same state it was when first connected.
+
+=head2 AutoCommit only
+
+Transactions aren't supported.
+
+=head1 CAVEATS
+
+A few things to keep in mind when using DBD::Gofer:
+
+=head2 Driver-private Methods
+
+These can be called via the func() method on the dbh
+but not the sth.
+
+=head2 Driver-private Statement Handle Attributes
+
+Driver-private sth attributes can be set in the prepare() call. XXX
+
+Driver-private sth attributes can't be read, currently. In future it will be
+possible to indicate which sth attributes you'd like to be able to read.
+
+=head1 Array Methods
+
+The array methods (bind_param_inout bind_param_array bind_param_inout_array 
execute_array execute_for_fetch)
+are not currently supported. Patches welcome, of course.
+
+=head1 Multiple Resultsets
+
+Multiple resultsets are supported if the driver supports the more_results() 
method.
+
+=head1 CONNECTING
+
+XXX
+
+=head2 Using DBI_AUTOPROXY
+
+XXX
+
+=head1 CONFIGURING VIA POLICY
+
+XXX
+
+=head1 AUTHOR AND COPYRIGHT
+
+The DBI module is Copyright (c) 2007 Tim Bunce. Ireland.
+All rights reserved.
+            
+You may distribute under the terms of either the GNU General Public
+License or the Artistic License, as specified in the Perl README file.
+
+=head1 SEE ALSO
+
+L<DBD::Gofer::Request>, L<DBD::Gofer::Response>, 
L<DBD::Gofer::Transport::Base>,
+
+L<DBI>, L<DBI::Gofer::Execute>.
+
+
+=head1 TODO
+
+dbh STORE doesn't record set attributes
+
+Driver-private sth attributes - set via prepare() - change DBI spec
+Auto-configure based on driver name.
+Automatically send back everything in sth attribute cache?
+
+Caching of get_info values
+
+prepare vs prepare_cached
+
+Driver-private sth methods via func? Can't be sure of state?
+
+Sybase specific features.
+
+XXX track installed_methods and install proxies on client side after connect?
+
+XXX add hooks into transport base class for checking & updating a cache
+   ie via a standard cache interface such as:
+   http://search.cpan.org/~robm/Cache-FastMmap/FastMmap.pm
+   http://search.cpan.org/~bradfitz/Cache-Memcached/lib/Cache/Memcached.pm
+   http://search.cpan.org/~dclinton/Cache-Cache/
+   http://search.cpan.org/~cleishman/Cache/
+
+Also caching instructions could be passed through the httpd transport layer
+in such a way that appropriate http cache headers are added to the results
+so that web caches (squid etc) could be used to implement the caching.
+(May require the use of GET rather than POST requests.)
+
+check clone tests
+
+add early test that connected dbh is active
+
+=cut

Added: dbi/trunk/lib/DBD/Gofer/Transport/Base.pm
==============================================================================
--- (empty file)
+++ dbi/trunk/lib/DBD/Gofer/Transport/Base.pm   Mon Jan 29 05:21:16 2007
@@ -0,0 +1,52 @@
+package DBD::Gofer::Transport::Base;
+
+#   $Id: Base.pm 8696 2007-01-24 23:12:38Z timbo $
+#
+#   Copyright (c) 2007, Tim Bunce, Ireland
+#
+#   You may distribute under the terms of either the GNU General Public
+#   License or the Artistic License, as specified in the Perl README file.
+
+use strict;
+use warnings;
+
+use Carp qw(cluck);
+use Storable qw(freeze thaw);
+
+use base qw(Class::Accessor::Fast);
+
+our $VERSION = sprintf("0.%06d", q$Revision: 8696 $ =~ /(\d+)/o);
+
+our $debug = $ENV{DBD_GOFER_TRACE} || 0;
+
+
+__PACKAGE__->mk_accessors(qw(
+    go_dsn
+));
+
+
+sub freeze_data {
+    my ($self, $data, $skip_debug) = @_;
+    $self->_dump("DBD_GOFER_TRACE freezing ".ref($data), $data)
+        if $debug && not $skip_debug;
+    local $Storable::forgive_me = 1; # for CODE refs etc
+    return freeze($data);
+}   
+
+sub thaw_data {
+    my ($self, $frozen_data, $skip_debug) = @_;
+    my $data = thaw($frozen_data);
+    $self->_dump("DBD_GOFER_TRACE thawing ".ref($data), $data)
+        if $debug && not $skip_debug;
+    return $data;
+}
+
+
+sub _dump {
+    my ($self, $label, $data) = @_;
+    require Data::Dumper;
+    # XXX dd settings
+    warn "$label=".Data::Dumper::Dumper($data);
+}
+
+1;

Added: dbi/trunk/lib/DBD/Gofer/Transport/null.pm
==============================================================================
--- (empty file)
+++ dbi/trunk/lib/DBD/Gofer/Transport/null.pm   Mon Jan 29 05:21:16 2007
@@ -0,0 +1,67 @@
+package DBD::Gofer::Transport::null;
+
+#   $Id: null.pm 8696 2007-01-24 23:12:38Z timbo $
+#
+#   Copyright (c) 2007, Tim Bunce, Ireland
+#
+#   You may distribute under the terms of either the GNU General Public
+#   License or the Artistic License, as specified in the Perl README file.
+
+use strict;
+use warnings;
+
+use base qw(DBD::Gofer::Transport::Base);
+
+use DBI::Gofer::Execute qw(execute_request);
+
+our $VERSION = sprintf("0.%06d", q$Revision: 8696 $ =~ /(\d+)/o);
+
+__PACKAGE__->mk_accessors(qw(
+    pending_response
+)); 
+
+
+sub transmit_request {
+    my ($self, $request) = @_;
+
+    my $frozen_request = $self->freeze_data($request);
+
+    # ...
+    # the request is magically transported over to ... ourselves
+    # ...
+
+    # since we're in the same process, we don't want to show the DBI trace
+    # enabled for the 'client' because it gets very hard to follow.
+    # So control the Gofer 'server' side independently
+    # but similar logic as used for DBI_TRACE parsing.
+    my $prev_trace_level = DBI->trace(
+        ($ENV{DBD_GOFER_NULL_TRACE}) ? (split /=/, $ENV{DBD_GOFER_NULL_TRACE}) 
: (0)
+    );
+
+    my $response = execute_request( $self->thaw_data($frozen_request,1) );
+
+    DBI->trace($prev_trace_level);
+
+    # put response 'on the shelf' ready for receive_response()
+    $self->pending_response( $response );
+
+    return 1;
+}
+
+
+sub receive_response {
+    my $self = shift;
+
+    my $response = $self->pending_response;
+
+    my $frozen_response = $self->freeze_data($response,1);
+
+    # ...
+    # the response is magically transported back to ... ourselves
+    # ...
+
+    return $self->thaw_data($frozen_response);
+}
+
+
+1;

Added: dbi/trunk/lib/DBI/Gofer/Execute.pm
==============================================================================
--- (empty file)
+++ dbi/trunk/lib/DBI/Gofer/Execute.pm  Mon Jan 29 05:21:16 2007
@@ -0,0 +1,215 @@
+package DBI::Gofer::Execute;
+
+#   $Id: Execute.pm 8696 2007-01-24 23:12:38Z timbo $
+#
+#   Copyright (c) 2007, Tim Bunce, Ireland
+#
+#   You may distribute under the terms of either the GNU General Public
+#   License or the Artistic License, as specified in the Perl README file.
+
+use strict;
+use warnings;
+
+use DBI;
+use DBI::Gofer::Request;
+use DBI::Gofer::Response;
+
+use base qw(Exporter);
+
+our $VERSION = sprintf("0.%06d", q$Revision: 8696 $ =~ /(\d+)/o);
+
+our @EXPORT_OK = qw(
+    execute_request
+    execute_dbh_request
+    execute_sth_request
+);
+
+our @sth_std_attr = qw(
+    NUM_OF_PARAMS
+    NUM_OF_FIELDS
+    NAME
+    TYPE
+    NULLABLE
+    PRECISION
+    SCALE
+    CursorName
+);
+
+our $trace = $ENV{DBI_GOFER_TRACE};
+
+our $recurse = 0;
+
+# XXX tracing
+
+sub _connect {
+    my $request = shift;
+    local $ENV{DBI_AUTOPROXY};
+    my $connect_args = $request->connect_args;
+    my ($dsn, $u, $p, $attr) = @$connect_args;
+    # delete attributes we don't want to affect the server-side
+    delete @{$attr}{qw(Profile InactiveDestroy Warn HandleError HandleSetErr 
TraceLevel Taint TaintIn TaintOut)};
+    my $connect_method = 'connect_cached';
+#$connect_method = 'connect';
+    # XXX need way to limit/purge connect cache over time
+    my $dbh = DBI->$connect_method($dsn, $u, $p, {
+        %$attr,
+        # force some attributes the way we want them
+        PrintWarn  => 0,
+        PrintError => 0,
+        RaiseError => 1,
+        # ensure this connect_cached doesn't have the same args as the client
+        # because that causes subtle issues if in the same process (ie 
transport=null)
+        dbi_go_execute_unique => 42+$recurse+rand(),
+    });
+    die "NOT CONNECTED" if $dbh and not $dbh->{Active};
+    #$dbh->trace(0);
+    return $dbh;
+}
+
+
+sub _reset_dbh {
+    my ($dbh) = @_;
+    $dbh->set_err(undef, undef); # clear any error state
+    #$dbh->trace(0, \*STDERR);
+}
+
+
+sub _new_response_with_err {
+    my ($rv) = @_;
+
+    my ($err, $errstr, $state) = ($DBI::err, $DBI::errstr, $DBI::state);
+    if ($@ and !$errstr || $@ !~ /^DBD::/) {
+        $err ||= 1;
+        $errstr = ($errstr) ? "$errstr; $@" : $@;
+    }
+    my $response = DBI::Gofer::Response->new({
+        rv     => $rv,
+        err    => $err,
+        errstr => $errstr,
+        state  => $state,
+    });
+    return $response;
+}
+
+
+sub execute_request {
+    my $request = shift;
+    local $recurse = $recurse + 1;
+    warn "Gofer request level $recurse\n" if $trace;
+    # guaranteed not to throw an exception
+    my $response = eval {
+        ($request->is_sth_request)
+            ? execute_sth_request($request)
+            : execute_dbh_request($request);
+    };
+    if ($@) {
+        warn $@; # XXX
+        chomp $@;
+        $response = DBI::Gofer::Response->new({
+            err => 1, errstr => $@, state  => '',
+        });
+    }
+    warn "Gofer response level $recurse: ".$response->rv."\n" if $trace;
+    return $response;
+}
+
+sub execute_dbh_request {
+    my $request = shift;
+    my $dbh;
+    my $rv_ref = eval {
+        $dbh = _connect($request);
+        my $meth = $request->dbh_method_name;
+        my $args = $request->dbh_method_args;
+        my @rv = ($request->dbh_wantarray)
+            ?        $dbh->$meth(@$args)
+            : scalar $dbh->$meth(@$args);
+        [EMAIL PROTECTED];
+    };
+    my $response = _new_response_with_err($rv_ref);
+    if ($dbh) {
+        $response->last_insert_id = $dbh->last_insert_id( @{ 
$request->dbh_last_insert_id_args })
+            if $rv_ref && $request->dbh_last_insert_id_args;
+        _reset_dbh($dbh);
+    }
+    if ($rv_ref and UNIVERSAL::isa($rv_ref->[0],'DBI::st')) {
+        my $rv = $rv_ref->[0];
+        # dbh_method_call was probably a metadata method like table_info
+        # that returns a statement handle, so turn the $sth into resultset
+        $response->sth_resultsets( _gather_sth_resultsets($rv, $request) );
+        $response->rv("(sth)");
+    }
+    if (0) {
+        # if not using connect_cached then we want to gracefu
+        local $SIG{__WARN__} = sub {};
+        undef $dbh;
+    }
+    return $response;
+}
+
+
+sub execute_sth_request {
+    my $request = shift;
+    my $dbh;
+    my $sth;
+
+    my $rv = eval {
+        $dbh = _connect($request);
+
+        my $meth = $request->dbh_method_name;
+        my $args = $request->dbh_method_args;
+        $sth = $dbh->$meth(@$args);
+        my $last = '(sth)'; # a true value
+
+        # execute methods on the sth, e.g., bind_param & execute
+        for my $meth_call (@{ $request->sth_method_calls }) {
+            my $method = shift @$meth_call;
+            $last = $sth->$method(@$meth_call);
+        }
+        $last;
+    };
+    my $response = _new_response_with_err($rv);
+
+    # even if the eval failed we still want to try to gather attribute values
+    $response->sth_resultsets( _gather_sth_resultsets($sth, $request) ) if 
$sth;
+
+    # XXX would be nice to be able to support streaming of results
+    # which would reduce memory usage and latency for large results
+
+    _reset_dbh($dbh) if $dbh;
+
+    return $response;
+}
+
+
+sub _gather_sth_resultsets {
+    my ($sth, $request) = @_;
+    return eval {
+        my $attr_list = $request->sth_result_attr;
+        $attr_list = [ keys %$attr_list ] if ref $attr_list eq 'HASH';
+        my $rs_list = [];
+        do {
+            my $rs = fetch_result_set($sth, $attr_list);
+            push @$rs_list, $rs;
+        } while $sth->more_results;
+
+        $rs_list;
+    };
+}
+
+
+sub fetch_result_set {
+    my ($sth, $extra_attr) = @_;
+    my %meta;
+    for my $attr (@sth_std_attr, @$extra_attr) {
+        $meta{ $attr } = $sth->{$attr};
+    }
+    if ($sth->FETCH('NUM_OF_FIELDS')) { # if a select
+        $meta{rowset} = eval { $sth->fetchall_arrayref() };
+        $meta{err}    = $DBI::err;
+        $meta{errstr} = $DBI::errstr;
+        $meta{state}  = $DBI::state;
+    }
+    return \%meta;
+}
+
+1;

Added: dbi/trunk/lib/DBI/Gofer/Request.pm
==============================================================================
--- (empty file)
+++ dbi/trunk/lib/DBI/Gofer/Request.pm  Mon Jan 29 05:21:16 2007
@@ -0,0 +1,43 @@
+package DBI::Gofer::Request;
+
+#   $Id: Request.pm 8696 2007-01-24 23:12:38Z timbo $
+#
+#   Copyright (c) 2007, Tim Bunce, Ireland
+#
+#   You may distribute under the terms of either the GNU General Public
+#   License or the Artistic License, as specified in the Perl README file.
+
+use base qw(Class::Accessor::Fast);
+
+our $VERSION = sprintf("0.%06d", q$Revision: 8696 $ =~ /(\d+)/o);
+
+
+__PACKAGE__->mk_accessors(qw(
+    connect_args
+    dbh_method_name
+    dbh_method_args
+    dbh_wantarray
+    dbh_last_insert_id_args
+    sth_method_calls
+    sth_result_attr
+));
+
+sub reset {
+    my $self = shift;
+    # remove everything except connect
+    %$self = ( connect_args => $self->{connect_args} );
+}
+
+sub is_sth_request {
+    return shift->{sth_result_attr};
+}
+
+sub init_request {
+    my ($self, $method, $args_ref, $wantarray) = @_;
+    $self->reset;
+    $self->dbh_method_name($method);
+    $self->dbh_method_args($args_ref);
+    $self->dbh_wantarray($wantarray);
+}
+
+1;

Added: dbi/trunk/lib/DBI/Gofer/Response.pm
==============================================================================
--- (empty file)
+++ dbi/trunk/lib/DBI/Gofer/Response.pm Mon Jan 29 05:21:16 2007
@@ -0,0 +1,23 @@
+package DBI::Gofer::Response;
+
+#   $Id: Response.pm 8696 2007-01-24 23:12:38Z timbo $
+#
+#   Copyright (c) 2007, Tim Bunce, Ireland
+#
+#   You may distribute under the terms of either the GNU General Public
+#   License or the Artistic License, as specified in the Perl README file.
+
+use base qw(Class::Accessor::Fast);
+
+our $VERSION = sprintf("0.%06d", q$Revision: 8696 $ =~ /(\d+)/o);
+
+__PACKAGE__->mk_accessors(qw(
+    rv
+    err
+    errstr
+    state
+    last_insert_id
+    sth_resultsets
+));
+
+1;

Added: dbi/trunk/lib/DBI/Gofer/Transport/Base.pm
==============================================================================
--- (empty file)
+++ dbi/trunk/lib/DBI/Gofer/Transport/Base.pm   Mon Jan 29 05:21:16 2007
@@ -0,0 +1,48 @@
+package DBD::Gofer::Transport::Base;
+
+#   $Id: Base.pm 8696 2007-01-24 23:12:38Z timbo $
+#
+#   Copyright (c) 2007, Tim Bunce, Ireland
+#
+#   You may distribute under the terms of either the GNU General Public
+#   License or the Artistic License, as specified in the Perl README file.
+
+use strict;
+use warnings;
+
+use Storable qw(freeze thaw);
+
+use base qw(Class::Accessor::Fast);
+
+our $VERSION = sprintf("0.%06d", q$Revision: 8696 $ =~ /(\d+)/o);
+
+our $debug = $ENV{DBI_GOFER_TRACE} || 0;
+
+
+__PACKAGE__->mk_accessors(qw(
+    go_dsn
+));
+
+
+sub freeze_data {
+    my ($self, $data) = @_;
+    $self->_dump("DBI_GOFER_TRACE ".ref($data), $data) if $debug;
+    local $Storable::forgive_me = 1; # for CODE refs etc
+    return freeze($data);
+}   
+
+sub thaw_data {
+    my ($self, $frozen_data) = @_;
+    my $data = thaw($frozen_data);
+    $self->_dump("DBI_GOFER_TRACE ".ref($data), $data) if $debug;
+    return $data;
+}
+
+
+sub _dump {
+    my ($self, $label, $data) = @_;
+    require Data::Dumper;
+    warn "$label=".Dumper($request);
+}
+
+1;

Added: dbi/trunk/t/85gofer.t
==============================================================================
--- (empty file)
+++ dbi/trunk/t/85gofer.t       Mon Jan 29 05:21:16 2007
@@ -0,0 +1,69 @@
+#!perl -w                                         # -*- perl -*-
+# vim:sw=4:ts=8
+
+use strict;
+use warnings;
+
+use Test::More 'no_plan';
+
+use DBI;
+
+use lib "/Users/timbo/dbi/trunk/lib";
+
+# use DBD::Gofer directly.
+# when combined with DBI_AUTOPROXY this means we have DBD::Gofer => DBD::Gofer 
=> DBD::DBM!
+#
+my $dsn = "dbi:Gofer:transport=null;dsn=dbi:DBM:dbm_type=SDBM_File;lockfile=0";
+my $dbh = DBI->connect($dsn);
+ok $dbh, 'should connect';
+
+
+    # 0=SQL::Statement if avail, 1=DBI::SQL::Nano
+    # next line forces use of Nano rather than default behaviour
+    $ENV{DBI_SQL_NANO}=1;
+
+#my $dir = './test_output';
+#rmtree $dir;
+#mkpath $dir;
+
+my @sql = split /\s*;\n/, join '',<DATA>;
+
+for my $sql ( @sql ) {
+    $sql =~ s/;$//;  # in case no final \n on last line of __DATA__
+    my $null = '';
+    my $expected_results = {
+        1 => 'oranges',
+        2 => 'apples',
+        3 => $null,
+    };
+    if ($sql !~ /SELECT/) {
+        print " do $sql\n";
+        $dbh->do($sql) or die $dbh->errstr;
+        next;
+    }
+    print " run $sql\n";
+    my $sth = $dbh->prepare($sql) or die $dbh->errstr;
+    $sth->execute;
+    die $sth->errstr if $sth->err and $sql !~ /DROP/;
+    # Note that we can't rely on the order here, it's not portable,
+    # different DBMs (or versions) will return different orders.
+    while (my ($key, $value) = $sth->fetchrow_array) {
+        ok exists $expected_results->{$key};
+        is $value, $expected_results->{$key};
+    }
+    is $DBI::rows, keys %$expected_results;
+}
+$dbh->disconnect;
+
+1;
+__DATA__
+DROP TABLE IF EXISTS fruit;
+CREATE TABLE fruit (dKey INT, dVal VARCHAR(10));
+INSERT INTO  fruit VALUES (1,'oranges'   );
+INSERT INTO  fruit VALUES (2,'to_change' );
+INSERT INTO  fruit VALUES (3, NULL       );
+INSERT INTO  fruit VALUES (4,'to delete' );
+UPDATE fruit SET dVal='apples' WHERE dKey=2;
+DELETE FROM  fruit WHERE dVal='to delete';
+SELECT * FROM fruit;
+DROP TABLE fruit;

Reply via email to