sl@0: # 2008 May 12 sl@0: # sl@0: # The author disclaims copyright to this source code. In place of sl@0: # a legal notice, here is a blessing: sl@0: # sl@0: # May you do good and not evil. sl@0: # May you find forgiveness for yourself and forgive others. sl@0: # May you share freely, never taking more than you give. sl@0: # sl@0: #*********************************************************************** sl@0: # sl@0: # This file tests that if sqlite3_release_memory() is called to reclaim sl@0: # memory from a pager that is in the error-state, SQLite does not sl@0: # incorrectly write dirty pages out to the database (not safe to do sl@0: # once the pager is in error state). sl@0: # sl@0: # $Id: ioerr5.test,v 1.5 2008/08/28 18:35:34 danielk1977 Exp $ sl@0: sl@0: set testdir [file dirname $argv0] sl@0: source $testdir/tester.tcl sl@0: sl@0: ifcapable !memorymanage||!shared_cache { sl@0: finish_test sl@0: return sl@0: } sl@0: sl@0: db close sl@0: sl@0: set ::enable_shared_cache [sqlite3_enable_shared_cache 1] sl@0: set ::soft_limit [sqlite3_soft_heap_limit 1048576] sl@0: sl@0: # This procedure prepares, steps and finalizes an SQL statement via the sl@0: # UTF-16 APIs. The text representation of an SQLite error code is returned sl@0: # ("SQLITE_OK", "SQLITE_IOERR" etc.). The actual results returned by the sl@0: # SQL statement, if it is a SELECT, are not available. sl@0: # sl@0: # This can be useful for testing because it forces SQLite to make an extra sl@0: # call to sqlite3_malloc() when translating from the supplied UTF-16 to sl@0: # the UTF-8 encoding used internally. sl@0: # sl@0: proc dosql16 {zSql {db db}} { sl@0: set sql [encoding convertto unicode $zSql] sl@0: append sql "\00\00" sl@0: set stmt [sqlite3_prepare16 $db $sql -1 {}] sl@0: sqlite3_step $stmt sl@0: set rc [sqlite3_finalize $stmt] sl@0: } sl@0: sl@0: proc compilesql16 {zSql {db db}} { sl@0: set sql [encoding convertto unicode $zSql] sl@0: append sql "\00\00" sl@0: set stmt [sqlite3_prepare16 $db $sql -1 {}] sl@0: set rc [sqlite3_finalize $stmt] sl@0: } sl@0: sl@0: # Open two database connections (handle db and db2) to database "test.db". sl@0: # sl@0: proc opendatabases {} { sl@0: catch {db close} sl@0: catch {db2 close} sl@0: sqlite3 db test.db sl@0: sqlite3 db2 test.db sl@0: db2 cache size 0 sl@0: db cache size 0 sl@0: execsql { sl@0: pragma page_size=512; sl@0: pragma auto_vacuum=2; sl@0: pragma cache_size=16; sl@0: } sl@0: } sl@0: sl@0: # Open two database connections and create a single table in the db. sl@0: # sl@0: do_test ioerr5-1.0 { sl@0: opendatabases sl@0: execsql { CREATE TABLE A(Id INTEGER, Name TEXT) } sl@0: } {} sl@0: sl@0: foreach locking_mode {normal exclusive} { sl@0: set nPage 2 sl@0: for {set iFail 1} {$iFail<200} {incr iFail} { sl@0: sqlite3_soft_heap_limit 1048576 sl@0: opendatabases sl@0: execsql { pragma locking_mode=exclusive } sl@0: set nRow [db one {SELECT count(*) FROM a}] sl@0: sl@0: # Dirty (at least) one of the pages in the cache. sl@0: do_test ioerr5-1.$locking_mode-$iFail.1 { sl@0: execsql { sl@0: BEGIN EXCLUSIVE; sl@0: INSERT INTO a VALUES(1, 'ABCDEFGHIJKLMNOP'); sl@0: } sl@0: } {} sl@0: sl@0: # Now try to commit the transaction. Cause an IO error to occur sl@0: # within this operation, which moves the pager into the error state. sl@0: # sl@0: set ::sqlite_io_error_persist 1 sl@0: set ::sqlite_io_error_pending $iFail sl@0: do_test ioerr5-1.$locking_mode-$iFail.2 { sl@0: set rc [catchsql {COMMIT}] sl@0: list sl@0: } {} sl@0: set ::sqlite_io_error_hit 0 sl@0: set ::sqlite_io_error_persist 0 sl@0: set ::sqlite_io_error_pending 0 sl@0: sl@0: # Read the contents of the database file into a Tcl variable. sl@0: # sl@0: set fd [open test.db] sl@0: fconfigure $fd -translation binary -encoding binary sl@0: set zDatabase [read $fd] sl@0: close $fd sl@0: sl@0: # Set a very low soft-limit and then try to compile an SQL statement sl@0: # from UTF-16 text. To do this, SQLite will need to reclaim memory sl@0: # from the pager that is in error state. Including that associated sl@0: # with the dirty page. sl@0: # sl@0: do_test ioerr5-1.$locking_mode-$iFail.3 { sl@0: set bt [btree_from_db db] sl@0: sqlite3_soft_heap_limit 1024 sl@0: compilesql16 "SELECT 10" sl@0: array set stats [btree_pager_stats $bt] sl@0: sl@0: # If the pager made it all the way to PAGER_SYNCED state, then sl@0: # both in-memory pages are clean. Following the calls to sl@0: # release_memory() that were made as part of the [compilesql16] sl@0: # above, there will be zero pages left in the cache. sl@0: # sl@0: # If the pager did not make it as far as PAGER_SYNCED, the two sl@0: # in memory pages are still dirty. So there will be 2 pages left sl@0: # in the cache following the release_memory() calls. sl@0: # sl@0: if {$stats(state)==5} { sl@0: set nPage 0 sl@0: } sl@0: expr {$stats(page)==$nPage} sl@0: } {1} sl@0: sl@0: # Ensure that nothing was written to the database while reclaiming sl@0: # memory from the pager in error state. sl@0: # sl@0: do_test ioerr5-1.$locking_mode-$iFail.4 { sl@0: set fd [open test.db] sl@0: fconfigure $fd -translation binary -encoding binary sl@0: set zDatabase2 [read $fd] sl@0: close $fd sl@0: expr {$zDatabase eq $zDatabase2} sl@0: } {1} sl@0: sl@0: if {$rc eq [list 0 {}]} { sl@0: do_test ioerr5.1-$locking_mode-$iFail.3 { sl@0: execsql { SELECT count(*) FROM a } sl@0: } [expr $nRow+1] sl@0: break sl@0: } sl@0: } sl@0: } sl@0: sl@0: # Make sure this test script doesn't leave any files open. sl@0: # sl@0: do_test ioerr5-1.X { sl@0: catch { db close } sl@0: catch { db2 close } sl@0: set sqlite_open_file_count sl@0: } 0 sl@0: sl@0: do_test ioerr5-2.0 { sl@0: sqlite3 db test.db sl@0: execsql { CREATE INDEX i1 ON a(id, name); } sl@0: } {} sl@0: sl@0: foreach locking_mode {exclusive normal} { sl@0: for {set iFail 1} {$iFail<200} {incr iFail} { sl@0: sqlite3_soft_heap_limit 1048576 sl@0: opendatabases sl@0: execsql { pragma locking_mode=exclusive } sl@0: set nRow [db one {SELECT count(*) FROM a}] sl@0: sl@0: do_test ioerr5-2.$locking_mode-$iFail.1 { sl@0: execsql { sl@0: BEGIN EXCLUSIVE; sl@0: INSERT INTO a VALUES(1, 'ABCDEFGHIJKLMNOP'); sl@0: } sl@0: } {} sl@0: sl@0: set ::sqlite_io_error_persist 1 sl@0: set ::sqlite_io_error_pending $iFail sl@0: sl@0: sqlite3_release_memory 10000 sl@0: sl@0: set error_hit $::sqlite_io_error_hit sl@0: set ::sqlite_io_error_hit 0 sl@0: set ::sqlite_io_error_persist 0 sl@0: set ::sqlite_io_error_pending 0 sl@0: if {$error_hit} { sl@0: do_test ioerr5-2.$locking_mode-$iFail.3a { sl@0: catchsql COMMIT sl@0: } {1 {disk I/O error}} sl@0: } else { sl@0: do_test ioerr5-2.$locking_mode-$iFail.3b { sl@0: execsql COMMIT sl@0: } {} sl@0: break sl@0: } sl@0: } sl@0: } sl@0: sl@0: # Make sure this test script doesn't leave any files open. sl@0: # sl@0: do_test ioerr5-2.X { sl@0: catch { db close } sl@0: catch { db2 close } sl@0: set sqlite_open_file_count sl@0: } 0 sl@0: sl@0: sqlite3_enable_shared_cache $::enable_shared_cache sl@0: sqlite3_soft_heap_limit $::soft_limit sl@0: sl@0: finish_test