[Rt-commit] r4373 - in DBIx-SearchBuilder/trunk: SearchBuilder

ruz at bestpractical.com ruz at bestpractical.com
Wed Jan 11 13:20:54 EST 2006


Author: ruz
Date: Wed Jan 11 13:20:52 2006
New Revision: 4373

Added:
   DBIx-SearchBuilder/trunk/t/03transactions.t
Modified:
   DBIx-SearchBuilder/trunk/SearchBuilder/Handle.pm

Log:
* EndTransaction method
* calculate nested transactions depth per dbh
* basic transactions tests
* update documentation

Modified: DBIx-SearchBuilder/trunk/SearchBuilder/Handle.pm
==============================================================================
--- DBIx-SearchBuilder/trunk/SearchBuilder/Handle.pm	(original)
+++ DBIx-SearchBuilder/trunk/SearchBuilder/Handle.pm	Wed Jan 11 13:20:52 2006
@@ -1,19 +1,19 @@
 # $Header: /home/jesse/DBIx-SearchBuilder/history/SearchBuilder/Handle.pm,v 1.21 2002/01/28 06:11:37 jesse Exp $
 package DBIx::SearchBuilder::Handle;
+
 use strict;
+use warnings;
+
 use Carp qw(croak cluck);
 use DBI;
 use Class::ReturnValue;
 use Encode;
 
-use vars qw($VERSION @ISA %DBIHandle $PrevHandle $DEBUG $TRANSDEPTH);
-
-$TRANSDEPTH = 0;
+use vars qw($VERSION @ISA %DBIHandle $PrevHandle $DEBUG %TRANSDEPTH);
 
 $VERSION = '$Version$';
 
 
-
 =head1 NAME
 
 DBIx::SearchBuilder::Handle - Perl extension which is a generic DBI handle
@@ -311,12 +311,11 @@
 =cut
 
 sub Disconnect  {
-  my $self = shift;
-  if ($self->dbh) {
-      return ($self->dbh->disconnect());
-  } else {
-      return;
-  }
+    my $self = shift;
+    my $dbh = $self->dbh;
+    return unless $dbh;
+    $self->Rollback(1);
+    return $dbh->disconnect;
 }
 
 
@@ -644,88 +643,118 @@
     return ($field, $operator, $value,undef);
 }
 
+=head2 Transactions
 
+L<DBIx::SearchBuilder::Handle> emulates nested transactions,
+by keeping a transaction stack depth.
 
+B<NOTE:> In nested transactions you shouldn't mix rollbacks and commits,
+because only last action really do commit/rollback. For example next code
+would produce desired results:
+
+  $handle->BeginTransaction;
+    $handle->BeginTransaction;
+    ...
+    $handle->Rollback;
+    $handle->BeginTransaction;
+    ...
+    $handle->Commit;
+  $handle->Commit;
 
-=head2 BeginTransaction
+Only last action(Commit in example) finilize transaction in DB.
 
-Tells DBIx::SearchBuilder to begin a new SQL transaction. This will
-temporarily suspend Autocommit mode.
+=head3 BeginTransaction
 
-Emulates nested transactions, by keeping a transaction stack depth.
+Tells DBIx::SearchBuilder to begin a new SQL transaction.
+This will temporarily suspend Autocommit mode.
 
 =cut
 
 sub BeginTransaction {
     my $self = shift;
-    $TRANSDEPTH++;
-    if ($TRANSDEPTH > 1 ) {
-        return ($TRANSDEPTH);
-    } else {
-       return($self->dbh->begin_work);
-    }
+
+    my $depth = $self->TransactionDepth;
+    return unless defined $depth;
+
+    $self->TransactionDepth(++$depth);
+    return 1 if $depth > 1;
+
+    return $self->dbh->begin_work;
 }
 
+=head3 EndTransaction [Action => 'commit'] [Force => 0]
 
+Tells to end the current transaction. Takes C<Action> argument
+that could be C<commit> or C<rollback>, the default value
+is C<commit>.
 
-=head2 Commit
+If C<Force> argument is true then all nested transactions
+would be committed or rolled back.
 
-Tells DBIx::SearchBuilder to commit the current SQL transaction. 
-This will turn Autocommit mode back on.
+If there is no transaction in progress then method throw
+warning unless action is forced.
+
+Method returns true on success or false if error occured.
 
 =cut
 
-sub Commit {
+sub EndTransaction {
     my $self = shift;
-    unless ($TRANSDEPTH) {Carp::confess("Attempted to commit a transaction with none in progress")};
-    $TRANSDEPTH--;
+    my %args = ( Action => 'commit', Force => 0, @_ );
+    my $action = lc $args{'Action'} eq 'commit'? 'commit': 'rollback';
 
-    if ($TRANSDEPTH == 0 ) {
-        return($self->dbh->commit);
-    } else { #we're inside a transaction
-        return($TRANSDEPTH);
+    my $depth = $self->TransactionDepth || 0;
+    unless ( $depth ) {
+        unless( $args{'Force'} ) {
+            Carp::cluck( "Attempted to $action a transaction with none in progress" );
+            return 0;
+        }
+        return 1;
+    } else {
+        $depth--;
     }
-}
-
+    $depth = 0 if $args{'Force'};
 
+    $self->TransactionDepth( $depth );
+    return 1 if $depth;
+    return $self->dbh->rollback unless $action eq 'commit';
+    return $self->dbh->commit;
+}
 
-=head2 Rollback [FORCE]
+=head3 Commit [FORCE]
 
-Tells DBIx::SearchBuilder to abort the current SQL transaction. 
-This will turn Autocommit mode back on.
+Tells to commit the current SQL transaction.
 
-If this method is passed a true argument, stack depth is blown away and the outermost transaction is rolled back
+Method uses C<EndTransaction> method, read its
+L<description|DBIx::SearchBuilder::Handle/EndTransaction>.
 
 =cut
 
-sub Rollback {
+sub Commit {
     my $self = shift;
-    my $force = shift;
+    $self->EndTransaction( Action => 'commit', Force => shift );
+}
 
-    my $dbh = $self->dbh;
-    unless( $dbh ) {
-        $TRANSDEPTH = 0;
-        return;
-    }
 
-    #unless ($TRANSDEPTH) {Carp::confess("Attempted to rollback a transaction with none in progress")};
-    if ($force) {
-        $TRANSDEPTH = 0;
-        return($dbh->rollback);
-    }
+=head3 Rollback [FORCE]
 
-    $TRANSDEPTH-- if ($TRANSDEPTH >= 1);
-    if ($TRANSDEPTH == 0 ) {
-        return($dbh->rollback);
-    } else { #we're inside a transaction
-        return($TRANSDEPTH);
-    }
+Tells to abort the current SQL transaction.
+
+Method uses C<EndTransaction> method, read its
+L<description|DBIx::SearchBuilder::Handle/EndTransaction>.
+
+=cut
+
+sub Rollback {
+    my $self = shift;
+    $self->EndTransaction( Action => 'rollback', Force => shift );
 }
 
 
-=head2 ForceRollback
+=head3 ForceRollback
 
-Force the handle to rollback. Whether or not we're deep in nested transactions
+Force the handle to rollback.
+Whether or not we're deep in nested transactions.
 
 =cut
 
@@ -735,24 +764,35 @@
 }
 
 
-=head2 TransactionDepth
+=head3 TransactionDepth
 
-Return the current depth of the faked nested transaction stack.
+Returns the current depth of the nested transaction stack.
+Returns C<undef> if there is no connection to database.
 
 =cut
 
 sub TransactionDepth {
     my $self = shift;
-    return ($TRANSDEPTH); 
-}
 
+    my $dbh = $self->dbh;
+    return undef unless $dbh && $dbh->ping;
+
+    if ( @_ ) {
+        my $depth = shift;
+        if ( $depth ) {
+            $TRANSDEPTH{ $dbh } = $depth;
+        } else {
+            delete $TRANSDEPTH{ $dbh };
+        }
+    }
+    return $TRANSDEPTH{ $dbh } || 0;
+}
 
 
 =head2 ApplyLimits STATEMENTREF ROWS_PER_PAGE FIRST_ROW
 
 takes an SQL SELECT statement and massages it to return ROWS_PER_PAGE starting with FIRST_ROW;
 
-
 =cut
 
 sub ApplyLimits {

Added: DBIx-SearchBuilder/trunk/t/03transactions.t
==============================================================================
--- (empty file)
+++ DBIx-SearchBuilder/trunk/t/03transactions.t	Wed Jan 11 13:20:52 2006
@@ -0,0 +1,177 @@
+#!/usr/bin/perl -w
+
+
+use strict;
+use warnings;
+use File::Spec;
+use Test::More;
+BEGIN { require "t/utils.pl" }
+our (@AvailableDrivers);
+
+use constant TESTS_PER_DRIVER => 42;
+
+my $total = scalar(@AvailableDrivers) * TESTS_PER_DRIVER;
+plan tests => $total;
+
+foreach my $d ( @AvailableDrivers ) {
+SKIP: {
+	unless( has_schema( 'TestApp::Address', $d ) ) {
+		skip "No schema for '$d' driver", TESTS_PER_DRIVER;
+	}
+	unless( should_test( $d ) ) {
+		skip "ENV is not defined for driver '$d'", TESTS_PER_DRIVER;
+	}
+
+	my $handle = get_handle( $d );
+    isa_ok($handle, 'DBIx::SearchBuilder::Handle');
+    { # clear PrevHandle
+        no warnings 'once';
+        $DBIx::SearchBuilder::Handle::PrevHandle = undef;
+    }
+
+diag("disconnected handle") if $ENV{'TEST_VERBOSE'};
+    is($handle->TransactionDepth, undef, "undefined transaction depth");
+    is($handle->BeginTransaction, undef, "couldn't begin transaction");
+    is($handle->TransactionDepth, undef, "still undefined transaction depth");
+    ok($handle->EndTransaction(Action => 'commit', Force => 1), "force commit success silently");
+    ok($handle->Commit('force'), "force commit success silently");
+    ok($handle->EndTransaction(Action => 'rollback', Force => 1), "force rollback success silently");
+    ok($handle->Rollback('force'), "force rollback success silently");
+    # XXX: ForceRollback function should deprecated
+    ok($handle->ForceRollback, "force rollback success silently");
+    {
+        my $warn = 0;
+        local $SIG{__WARN__} = sub{ $_[0] =~ /transaction with none in progress/? $warn++: warn @_ };
+        ok(!$handle->Rollback, "not forced rollback returns false");
+        is($warn, 1, "not forced rollback fires warning");
+        ok(!$handle->Commit, "not forced commit returns false");
+        is($warn, 2, "not forced commit fires warning");
+    }
+
+	connect_handle( $handle );
+	isa_ok($handle->dbh, 'DBI::db');
+
+diag("connected handle without transaction") if $ENV{'TEST_VERBOSE'};
+    is($handle->TransactionDepth, 0, "transaction depth is 0");
+    ok($handle->Commit('force'), "force commit success silently");
+    ok($handle->Rollback('force'), "force rollback success silently");
+    {
+        my $warn = 0;
+        local $SIG{__WARN__} = sub{ $_[0] =~ /transaction with none in progress/? $warn++: warn @_ };
+        ok(!$handle->Rollback, "not forced rollback returns false");
+        is($warn, 1, "not forced rollback fires warning");
+        ok(!$handle->Commit, "not forced commit returns false");
+        is($warn, 2, "not forced commit fires warning");
+    }
+
+diag("begin and commit empty transaction") if $ENV{'TEST_VERBOSE'};
+    ok($handle->BeginTransaction, "begin transaction");
+    is($handle->TransactionDepth, 1, "transaction depth is 1");
+    ok($handle->Commit, "commit successed");
+    is($handle->TransactionDepth, 0, "transaction depth is 0");
+
+diag("begin and rollback empty transaction") if $ENV{'TEST_VERBOSE'};
+    ok($handle->BeginTransaction, "begin transaction");
+    is($handle->TransactionDepth, 1, "transaction depth is 1");
+    ok($handle->Rollback, "rollback successed");
+    is($handle->TransactionDepth, 0, "transaction depth is 0");
+
+diag("nested empty transactions") if $ENV{'TEST_VERBOSE'};
+    ok($handle->BeginTransaction, "begin transaction");
+    is($handle->TransactionDepth, 1, "transaction depth is 1");
+    ok($handle->BeginTransaction, "begin nested transaction");
+    is($handle->TransactionDepth, 2, "transaction depth is 2");
+    ok($handle->Commit, "commit successed");
+    is($handle->TransactionDepth, 1, "transaction depth is 1");
+    ok($handle->Commit, "commit successed");
+    is($handle->TransactionDepth, 0, "transaction depth is 0");
+
+diag("init schema in transaction and commit") if $ENV{'TEST_VERBOSE'};
+    # MySQL doesn't support transactions for CREATE TABLE
+    # so it's fake transactions test
+    ok($handle->BeginTransaction, "begin transaction");
+    is($handle->TransactionDepth, 1, "transaction depth is 1");
+	my $ret = init_schema( 'TestApp::Address', $handle );
+	isa_ok($ret, 'DBI::st', "Inserted the schema. got a statement handle back");
+    ok($handle->Commit, "commit successed");
+    is($handle->TransactionDepth, 0, "transaction depth is 0");
+
+	cleanup_schema( 'TestApp::Address', $handle );
+}} # SKIP, foreach blocks
+
+1;
+
+
+
+package TestApp::Address;
+
+use base qw/DBIx::SearchBuilder::Record/;
+
+sub _Init {
+    my $self = shift;
+    my $handle = shift;
+    $self->Table('Address');
+    $self->_Handle($handle);
+}
+
+sub ValidateName
+{
+	my ($self, $value) = @_;
+	return 0 if $value =~ /invalid/i;
+	return 1;
+}
+
+sub _ClassAccessible {
+
+    {   
+        
+        id =>
+        {read => 1, type => 'int(11)', default => ''}, 
+        Name => 
+        {read => 1, write => 1, type => 'varchar(14)', default => ''},
+        Phone => 
+        {read => 1, write => 1, type => 'varchar(18)', length => 18, default => ''},
+        EmployeeId => 
+        {read => 1, write => 1, type => 'int(8)', default => ''},
+
+}
+
+}
+
+sub schema_mysql {
+<<EOF;
+CREATE TEMPORARY TABLE Address (
+        id integer AUTO_INCREMENT,
+        Name varchar(36),
+        Phone varchar(18),
+        EmployeeId int(8),
+  	PRIMARY KEY (id)) TYPE='InnoDB'
+EOF
+
+}
+
+sub schema_pg {
+<<EOF;
+CREATE TEMPORARY TABLE Address (
+        id serial PRIMARY KEY,
+        Name varchar,
+        Phone varchar,
+        EmployeeId integer
+)
+EOF
+
+}
+
+sub schema_sqlite {
+
+<<EOF;
+CREATE TABLE Address (
+        id  integer primary key,
+        Name varchar(36),
+        Phone varchar(18),
+        EmployeeId int(8))
+EOF
+
+}
+
+1;


More information about the Rt-commit mailing list