#13 Finish cached statements when sqlite-finalize is called on them

Merged
dannym merged 1 commits from reepca/master into guile-sqlite3/master 3 years ago
1 changed files with 34 additions and 9 deletions
  1. 34 9
      sqlite3.scm.in

+ 34 - 9
sqlite3.scm.in

@@ -274,15 +274,40 @@ statements, into DB.  The result is unspecified."
             (dynamic-func "sqlite3_finalize" libsqlite3)
             (list '*))))
     (lambda (stmt)
-      ;; Note: When STMT is cached, this is a no-op.  This ensures caching
-      ;; actually works while still separating concerns: users can turn
-      ;; caching on and off without having to change the rest of their code.
-      (when (and (stmt-live? stmt)
-                 (not (stmt-cached? stmt)))
-        (let ((p (stmt-pointer stmt)))
-          (sqlite-remove-statement! (stmt->db stmt) stmt)
-          (set-stmt-live?! stmt #f)
-          (f p))))))
+      ;; Note: When STMT is cached, this merely resets.  This ensures caching
+      ;; actually works while still separating concerns: users can turn caching
+      ;; on and off without having to change the rest of their code.
+      (if (and (stmt-live? stmt)
+               (not (stmt-cached? stmt)))
+          (let ((p (stmt-pointer stmt)))
+            (sqlite-remove-statement! (stmt->db stmt) stmt)
+            (set-stmt-live?! stmt #f)
+            (f p))
+          ;; It's necessary to reset cached statements due to the following:
+          ;;
+          ;; "An implicit transaction (a transaction that is started
+          ;; automatically, not a transaction started by BEGIN) is committed
+          ;; automatically when the last active statement finishes.  A statement
+          ;; finishes when its last cursor closes, which is guaranteed to happen
+          ;; when the prepared statement is reset or finalized.  Some statements
+          ;; might "finish" for the purpose of transaction control prior to
+          ;; being reset or finalized, but there is no guarantee of this."
+          ;;
+          ;; (see https://www.sqlite.org/lang_transaction.html)
+          ;;
+          ;; Thus, it's possible for an implicitly-started transaction to hang
+          ;; around until sqlite-reset is called when the cached statement is
+          ;; next used.  Because the transaction is committed automatically only
+          ;; when the *last active statement* finishes, the implicitly-started
+          ;; transaction may later be upgraded to a write transaction (!) and
+          ;; this non-reset statement will still be keeping the transaction from
+          ;; committing until it is next used or the database connection is
+          ;; closed.  This has the potential to make (exclusive) write access to
+          ;; the database necessary for much longer than it should be.
+          ;;
+          ;; So it's necessary to preserve the statement-finishing behavior of
+          ;; sqlite_finalize here, which we do by calling sqlite-reset.
+          (sqlite-reset stmt)))))
 
 (define *stmt-map* (make-weak-key-hash-table))
 (define (stmt->db stmt)