Index: src/pager.c ================================================================== --- src/pager.c +++ src/pager.c @@ -334,11 +334,10 @@ u8 fullSync; /* Do extra syncs of the journal for robustness */ u8 sync_flags; /* One of SYNC_NORMAL or SYNC_FULL */ u8 tempFile; /* zFilename is a temporary file */ u8 readOnly; /* True for a read-only database */ u8 memDb; /* True to inhibit all file I/O */ - u8 safeJrnlHandle; /* True if jrnl may be held open with no lock */ /* The following block contains those class members that are dynamically ** modified during normal operations. The other variables in this structure ** are either constant throughout the lifetime of the pager, or else ** used to store configuration parameters that affect the way the pager @@ -1218,18 +1217,24 @@ ** treated as a hot-journal and rolled back. */ static void pager_unlock(Pager *pPager){ if( !pPager->exclusiveMode ){ int rc = SQLITE_OK; /* Return code */ + int iDc = isOpen(pPager->fd)?sqlite3OsDeviceCharacteristics(pPager->fd):0; /* Always close the journal file when dropping the database lock. ** Otherwise, another connection with journal_mode=delete might ** delete the file out from under us. */ - if( pPager->safeJrnlHandle==0 - || (pPager->journalMode!=PAGER_JOURNALMODE_TRUNCATE - && pPager->journalMode!=PAGER_JOURNALMODE_PERSIST) + assert( (PAGER_JOURNALMODE_MEMORY & 5)!=1 ); + assert( (PAGER_JOURNALMODE_OFF & 5)!=1 ); + assert( (PAGER_JOURNALMODE_WAL & 5)!=1 ); + assert( (PAGER_JOURNALMODE_DELETE & 5)!=1 ); + assert( (PAGER_JOURNALMODE_TRUNCATE & 5)==1 ); + assert( (PAGER_JOURNALMODE_PERSIST & 5)==1 ); + if( 0==(iDc & SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN) + || 1!=(pPager->journalMode & 5) ){ sqlite3OsClose(pPager->jfd); } sqlite3BitvecDestroy(pPager->pInJournal); @@ -3094,11 +3099,10 @@ disable_simulated_io_errors(); sqlite3BeginBenignMalloc(); pPager->errCode = 0; pPager->exclusiveMode = 0; - pPager->safeJrnlHandle = 0; #ifndef SQLITE_OMIT_WAL sqlite3WalClose(pPager->pWal, (pPager->noSync ? 0 : pPager->sync_flags), pPager->pageSize, pTmp ); @@ -3121,10 +3125,11 @@ } sqlite3EndBenignMalloc(); enable_simulated_io_errors(); PAGERTRACE(("CLOSE %d\n", PAGERID(pPager))); IOTRACE(("CLOSE %p\n", pPager)) + sqlite3OsClose(pPager->jfd); sqlite3OsClose(pPager->fd); sqlite3PageFree(pTmp); sqlite3PcacheClose(pPager->pPCache); #ifdef SQLITE_HAS_CODEC @@ -4505,14 +4510,10 @@ rc = sqlite3JournalOpen( pVfs, pPager->zJournal, pPager->jfd, flags, jrnlBufferSize(pPager) ); #else rc = sqlite3OsOpen(pVfs, pPager->zJournal, pPager->jfd, flags, 0); - if( rc==SQLITE_OK ){ - int iDc = sqlite3OsDeviceCharacteristics(pPager->jfd); - pPager->safeJrnlHandle = (iDc&SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN)!=0; - } #endif } assert( rc!=SQLITE_OK || isOpen(pPager->jfd) ); } Index: test/malloc_common.tcl ================================================================== --- test/malloc_common.tcl +++ test/malloc_common.tcl @@ -134,18 +134,18 @@ proc faultsim_save_and_close {} { faultsim_save catch { db close } return "" } -proc faultsim_restore_and_reopen {} { +proc faultsim_restore_and_reopen {{dbfile test.db}} { catch { db close } foreach f [glob -nocomplain test.db*] { file delete -force $f } foreach f2 [glob -nocomplain sv_test.db*] { set f [string range $f2 3 end] file copy -force $f2 $f } - sqlite3 db test.db + sqlite3 db $dbfile sqlite3_extended_result_codes db 1 sqlite3_db_config_lookaside db 0 0 0 } proc faultsim_integrity_check {{db db}} { @@ -154,11 +154,11 @@ } proc faultsim_delete_and_reopen {{file test.db}} { catch { db close } foreach f [glob -nocomplain test.db*] { file delete -force $f } - sqlite3 db test.db + sqlite3 db $file } # The following procs are used as [do_one_faultsim_test] callbacks when # injecting OOM faults into test cases. Index: test/pager1.test ================================================================== --- test/pager1.test +++ test/pager1.test @@ -225,10 +225,17 @@ # journal file is made). # # pager1.4.3.*: Test that the contents of a hot-journal are ignored if the # page-size or sector-size in the journal header appear to # be invalid (too large, too small or not a power of 2). +# +# pager1.4.4.*: Test hot-journal rollback of journal file with a master +# journal pointer generated in various "PRAGMA synchronous" +# modes. +# +# pager1.4.5.*: Test that hot-journal rollback stops if it encounters a +# journal-record for which the checksum fails. # do_test pager1.4.1.1 { faultsim_delete_and_reopen execsql { CREATE TABLE x(y, z); @@ -351,10 +358,150 @@ hexio_write test.db-journal $ofst [format %.8x $value] execsql { SELECT * FROM t1 } } $result } db close + +# Set up a VFS that snapshots the file-system just before a master journal +# file is deleted to commit a multi-file transaction. Specifically, the +# file-system is saved just before the xDelete() call to remove the +# master journal file from the file-system. +# +testvfs tv -default 1 +tv script copy_on_mj_delete +set ::mj_filename_length 0 +proc copy_on_mj_delete {method filename args} { + if {[string match *mj* [file tail $filename]]} { + set ::mj_filename_length [string length $filename] + faultsim_save + } + return SQLITE_OK +} + +set pwd [pwd] +foreach {tn1 tcl} { + 1 { set prefix "test.db" } + 2 { + # This test depends on the underlying VFS being able to open paths + # 512 bytes in length. The idea is to create a hot-journal file that + # contains a master-journal pointer so large that it could contain + # a valid page record (if the file page-size is 512 bytes). So as to + # make sure SQLite doesn't get confused by this. + # + set nPadding [expr 511 - $::mj_filename_length] + + # We cannot just create a really long database file name to open, as + # Linux limits a single component of a path to 255 bytes by default + # (and presumably other systems have limits too). So create a directory + # hierarchy to work in. + # + set dirname "d123456789012345678901234567890/" + set nDir [expr $nPadding / 32] + if { $nDir } { + set p [string repeat $dirname $nDir] + file mkdir $p + cd $p + } + + set padding [string repeat x [expr $nPadding %32]] + set prefix "test.db${padding}" + } +} { + eval $tcl + foreach {tn2 sql} { + o { + PRAGMA main.synchronous=OFF; + PRAGMA aux.synchronous=OFF; + } + o512 { + PRAGMA main.synchronous=OFF; + PRAGMA aux.synchronous=OFF; + PRAGMA main.page_size = 512; + PRAGMA aux.page_size = 512; + } + n { + PRAGMA main.synchronous=NORMAL; + PRAGMA aux.synchronous=NORMAL; + } + f { + PRAGMA main.synchronous=FULL; + PRAGMA aux.synchronous=FULL; + } + } { + + set tn "${tn1}.${tn2}" + + # Set up a connection to have two databases, test.db (main) and + # test.db2 (aux). Then run a multi-file transaction on them. The + # VFS will snapshot the file-system just before the master-journal + # file is deleted to commit the transaction. + # + tv filter xDelete + do_test pager1-4.4.$tn.1 { + faultsim_delete_and_reopen $prefix + execsql " + ATTACH '${prefix}2' AS aux; + $sql + CREATE TABLE a(x); + CREATE TABLE aux.b(x); + INSERT INTO a VALUES('double-you'); + INSERT INTO a VALUES('why'); + INSERT INTO a VALUES('zed'); + INSERT INTO b VALUES('won'); + INSERT INTO b VALUES('too'); + INSERT INTO b VALUES('free'); + " + execsql { + BEGIN; + INSERT INTO a SELECT * FROM b WHERE rowid<=3; + INSERT INTO b SELECT * FROM a WHERE rowid<=3; + COMMIT; + } + } {} + tv filter {} + + # Check that the transaction was committed successfully. + # + do_execsql_test pager1-4.4.$tn.2 { + SELECT * FROM a + } {double-you why zed won too free} + do_execsql_test pager1-4.4.$tn.3 { + SELECT * FROM b + } {won too free double-you why zed} + + # Restore the file-system and reopen the databases. Check that it now + # appears that the transaction was not committed (because the file-system + # was restored to the state where it had not been). + # + do_test pager1-4.4.$tn.4 { + faultsim_restore_and_reopen $prefix + execsql "ATTACH '${prefix}2' AS aux" + } {} + do_execsql_test pager1-4.4.$tn.5 {SELECT * FROM a} {double-you why zed} + do_execsql_test pager1-4.4.$tn.6 {SELECT * FROM b} {won too free} + + # Restore the file-system again. This time, before reopening the databases, + # delete the master-journal file from the file-system. It now appears that + # the transaction was committed (no master-journal file == no rollback). + # + do_test pager1-4.4.$tn.7 { + faultsim_restore_and_reopen $prefix + foreach f [glob ${prefix}-mj*] { file delete -force $f } + execsql "ATTACH '${prefix}2' AS aux" + } {} + do_execsql_test pager1-4.4.$tn.8 { + SELECT * FROM a + } {double-you why zed won too free} + do_execsql_test pager1-4.4.$tn.9 { + SELECT * FROM b + } {won too free double-you why zed} + } + + cd $pwd +} +db close +tv delete #------------------------------------------------------------------------- # The following tests deal with multi-file commits. # # pager1-5.1.*: The case where a multi-file cannot be committed because @@ -371,11 +518,14 @@ # is allocated between the last journal record and the # master-journal file name so that the master-journal file # name does not lie on the same sector as the last journal file # record. # -# pager1-5.5.*: +# pager1-5.5.*: Check that in journal_mode=PERSIST mode, a journal file is +# truncated to zero bytes when a multi-file transaction is +# committed (instead of the first couple of bytes being zeroed). +# # do_test pager1-5.1.1 { faultsim_delete_and_reopen execsql { ATTACH 'test.db2' AS aux;