In sqlite, ROLLBACK fails with SQLITE_BUSY when there are active/unfinished
SELECT statements.
As DBI's $dbh->rollback cannot handle errors in sane way, add workaround to
reset all sth and retry when $dbh->rollback fails with SQLITE_BUSY.

PS For whatever reasons, "Error handling, part 2" cannot be sent to list
(disappear into /dev/null without any diagnostics) :-|
Will retry in few days if it won't appear in gmane or list archives.
>From 0eb49f4b8cf4f67bb80447c0dc04b5e84ef26763 Mon Sep 17 00:00:00 2001
From: "Yuriy M. Kaminskiy" <yum...@gmail.com>
Date: Fri, 14 Oct 2011 22:31:37 +0400
Subject: [PATCH 1/2] Document interaction $sth->finish and ROLLBACK

---
 lib/DBD/SQLite.pm |   25 +++++++++++++++++++++++++
 1 files changed, 25 insertions(+), 0 deletions(-)

diff --git a/lib/DBD/SQLite.pm b/lib/DBD/SQLite.pm
index 094b588..5abaaaa 100644
--- a/lib/DBD/SQLite.pm
+++ b/lib/DBD/SQLite.pm
@@ -952,6 +952,31 @@ Note that this works only when all of the connections use the same
 (non-deferred) transaction. See L<http://sqlite.org/lockingv3.html>
 for locking details.
 
+=head2 C<< $sth->finish >> and Transaction Rollback
+
+Contrary to documentation on L<DBI/finish>, finishing active
+C<SELECT> statements is not optional with sqlite, otherwise
+C<ROLLBACK> will fail.
+
+  $sth = $dbh->prepare("SELECT * FROM t");
+  $dbh->begin_work;
+  eval {
+      $sth->execute;
+      $row = $sth->fetch;
+      ...
+      die "For some reason";
+      ...
+  };
+  if($@) {
+     $dbh->rollback; # UNEXPECTEDLY FAILS!
+  } else {
+     $dbh->commit;
+  }
+
+With database modification statements returning no data
+(C<INSERT>/C<DELETE>/C<UPDATE>), C<< $sth->finish >> is optional
+(and no-op), as with other DBI drivers.
+
 =head2 Processing Multiple Statements At A Time
 
 L<DBI>'s statement handle is not supposed to process multiple
-- 
1.7.6.4

>From df462e70bec30b00d7bf11acc5d4e8213df769f5 Mon Sep 17 00:00:00 2001
From: "Yuriy M. Kaminskiy" <yum...@gmail.com>
Date: Fri, 14 Oct 2011 23:43:13 +0400
Subject: [PATCH 2/2] Add workaround to reset all statements on $dbh->rollback
 failure

DBI don't expect and cannot correctly handle error in $dbh->rollback,
thus we are adding workaround to prevent this very surprising error
with nasty consequences.
$dbh->do("ROLLBACK") still produces error, but that's should be more
expected (and proper error handling in this case is not blocked by DBI).
---
 dbdimp.c                         |   33 +++++++++++++++++++-
 lib/DBD/SQLite.pm                |    6 ++--
 t/49_rollback_with_active_stmt.t |   62 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 97 insertions(+), 4 deletions(-)
 create mode 100644 t/49_rollback_with_active_stmt.t

diff --git a/dbdimp.c b/dbdimp.c
index f77d93c..7d90f29 100644
--- a/dbdimp.c
+++ b/dbdimp.c
@@ -358,11 +358,42 @@ sqlite_db_rollback(SV *dbh, imp_dbh_t *imp_dbh)
     croak_if_db_is_null();
 
     if (!sqlite3_get_autocommit(imp_dbh->db)) {
+        char *errmsg = NULL;
 
         sqlite_trace(dbh, imp_dbh, 3, "ROLLBACK TRAN");
 
-        rc = sqlite_exec(dbh, "ROLLBACK TRANSACTION");
+        rc = sqlite3_exec(imp_dbh->db,
+                          "ROLLBACK TRANSACTION",
+                          NULL, NULL, &errmsg);
+
+        if (rc == SQLITE_BUSY && DBIc_ACTIVE_KIDS(imp_dbh)) {
+            sqlite3_stmt *pStmt = NULL;
+
+            warn("ROLLBACK failed: %s, trying to reset all statements and retry (there are %d active statement handle(s))", errmsg, DBIc_ACTIVE_KIDS(imp_dbh));
+            if (errmsg) {
+                sqlite3_free(errmsg);
+                errmsg = NULL;
+            }
+
+            /* COMPAT: sqlite3_next_stmt is only available for 3006000 or newer */
+            while ((pStmt = sqlite3_next_stmt(imp_dbh->db, pStmt)) != NULL) {
+		/* FIXME:
+		 * This leaves $sth in somewhat confused state;
+		 * It would be better to iterate through active DBI sth's and
+		 * call sqlite_st_finish(), but I have no idea how.
+		 */
+                sqlite3_reset(pStmt);
+            }
+            sqlite_trace(dbh, imp_dbh, 3, "ROLLBACK TRAN [retry]");
+            rc = sqlite3_exec(imp_dbh->db,
+                              "ROLLBACK TRANSACTION",
+                              NULL, NULL, &errmsg);
+        }
+
         if (rc != SQLITE_OK) {
+            sqlite_error(dbh, rc, errmsg);
+            if (errmsg) sqlite3_free(errmsg);
+
             return FALSE; /* -> &sv_no in SQLite.xsi */
         }
     }
diff --git a/lib/DBD/SQLite.pm b/lib/DBD/SQLite.pm
index 5abaaaa..76576f8 100644
--- a/lib/DBD/SQLite.pm
+++ b/lib/DBD/SQLite.pm
@@ -959,7 +959,7 @@ C<SELECT> statements is not optional with sqlite, otherwise
 C<ROLLBACK> will fail.
 
   $sth = $dbh->prepare("SELECT * FROM t");
-  $dbh->begin_work;
+  $dbh->do("BEGIN");
   eval {
       $sth->execute;
       $row = $sth->fetch;
@@ -968,9 +968,9 @@ C<ROLLBACK> will fail.
       ...
   };
   if($@) {
-     $dbh->rollback; # UNEXPECTEDLY FAILS!
+     $dbh->do("ROLLBACK"); # UNEXPECTEDLY FAILS!
   } else {
-     $dbh->commit;
+     $dbh->do("COMMIT");
   }
 
 With database modification statements returning no data
diff --git a/t/49_rollback_with_active_stmt.t b/t/49_rollback_with_active_stmt.t
new file mode 100644
index 0000000..d010539
--- /dev/null
+++ b/t/49_rollback_with_active_stmt.t
@@ -0,0 +1,62 @@
+#!/usr/bin/perl
+
+# Trigger error on ROLLBACK, test workaround in $dbh->rollback
+
+use strict;
+BEGIN {
+	$|  = 1;
+	$^W = 1;
+}
+
+use t::lib::Test qw/connect_ok dbfile/;
+use Test::More;
+use Test::NoWarnings;
+
+plan tests => 9 + 1;
+
+my $dbh = connect_ok(
+    dbfile     => 'foo',
+    RaiseError => 1,
+    PrintError => 0,
+    AutoCommit => 1,
+);
+
+my $dbfile = dbfile('foo');
+
+$dbh->do("CREATE TABLE Blah ( id INTEGER )");
+$dbh->do("INSERT INTO Blah VALUES ( 1 )");
+$dbh->do("INSERT INTO Blah VALUES ( 2 )");
+$dbh->do("INSERT INTO Blah VALUES ( 3 )");
+
+my $sth;
+
+ok(($sth = $dbh->prepare("SELECT id FROM Blah")), "prepare");
+$dbh->do("BEGIN");
+$dbh->do("INSERT INTO Blah VALUES ( 4 )");
+
+$sth->execute;
+ok(ref($sth->fetch), "fetch");
+eval {
+    $dbh->do("ROLLBACK");
+};
+ok($@, "ROLLBACK fails with active SELECT statement");
+ok(ref($sth->fetch), "fetch 2nd row");
+if ($@) {
+    print "# expected execute failure : $@";
+}
+ok($sth->finish, "finish()");
+$dbh->do("ROLLBACK");
+$dbh->begin_work;
+$sth->execute;
+ok(ref($sth->fetch), "fetch");
+{
+    local $SIG{__WARN__} = sub {print "# expected warning : ",@_};
+    eval {$dbh->rollback};
+}
+ok(!$@, "rollback successful");
+ok($sth->finish, "finish()");
+$dbh->disconnect;
+
+undef($dbh);
+
+unlink $dbfile;
-- 
1.7.6.4

_______________________________________________
DBD-SQLite mailing list
DBD-SQLite@lists.scsys.co.uk
http://lists.scsys.co.uk/cgi-bin/mailman/listinfo/dbd-sqlite

Reply via email to