# 2010 May 5
#
# The author disclaims copyright to this source code.  In place of
# a legal notice, here is a blessing:
#
#    May you do good and not evil.
#    May you find forgiveness for yourself and forgive others.
#    May you share freely, never taking more than you give.
#
#***********************************************************************
# This file implements regression tests for SQLite library.  The
# focus of this file is testing the operation of the library in
# "PRAGMA journal_mode=WAL" mode.
#

set testdir [file dirname $argv0]
source $testdir/tester.tcl
source $testdir/lock_common.tcl
ifcapable !wal {finish_test ; return }

proc set_tvfs_hdr {file args} {
  if {[llength $args]>1} {
    return -code error {wrong # args: should be "set_tvfs_hdr fileName ?val?"}
  }

  set blob [tvfs shm $file]
  if {[llength $args]} {
    set blob [binary format i8a* [lindex $args 0] [string range $blob 32 end]]
    tvfs shm $file $blob
  }

  binary scan $blob i8 ints
  return $ints
}

proc incr_tvfs_hdr {file idx incrval} {
  set ints [set_tvfs_hdr $file]
  set v [lindex $ints $idx]
  incr v $incrval
  lset ints $idx $v
  set_tvfs_hdr $file $ints
}


#-------------------------------------------------------------------------
# Test case wal2-1.*:
#
# Set up a small database containing a single table. The database is not
# checkpointed during the test - all content resides in the log file.
#
# Two connections are established to the database file - a writer ([db])
# and a reader ([db2]). For each of the 8 integer fields in the wal-index
# header (6 fields and 2 checksum values), do the following:
#
#   1. Modify the database using the writer.
#
#   2. Attempt to read the database using the reader. Before the reader
#      has a chance to snapshot the wal-index header, increment one
#      of the the integer fields (so that the reader ends up with a corrupted
#      header).
#
#   3. Check that the reader recovers the wal-index and reads the correct
#      database content.
#
do_test wal2-1.0 {
  proc tvfs_cb {method args} { return SQLITE_OK }
  testvfs tvfs tvfs_cb

  sqlite3 db  test.db -vfs tvfs
  sqlite3 db2 test.db -vfs tvfs

  execsql {
    PRAGMA journal_mode = WAL;
    CREATE TABLE t1(a);
  } db2
  execsql {
    INSERT INTO t1 VALUES(1);
    INSERT INTO t1 VALUES(2);
    INSERT INTO t1 VALUES(3);
    INSERT INTO t1 VALUES(4);
    SELECT count(a), sum(a) FROM t1;
  }
} {4 10}
do_test wal2-1.1 {
  execsql { SELECT count(a), sum(a) FROM t1 } db2
} {4 10}

foreach {tn iInsert res wal_index_hdr_mod wal_locks} {
         2    5   {5 15}    0             {READ RECOVER READ UNLOCK}
         3    6   {6 21}    1             {READ RECOVER READ UNLOCK}
         4    7   {7 28}    2             {READ RECOVER READ UNLOCK}
         5    8   {8 36}    3             {READ RECOVER READ UNLOCK}
         6    9   {9 45}    4             {READ RECOVER READ UNLOCK}
         7   10   {10 55}   5             {READ RECOVER READ UNLOCK}
         8   11   {11 66}   6             {READ RECOVER READ UNLOCK}
         9   12   {12 78}   7             {READ RECOVER READ UNLOCK}
        10   13   {13 91}   -1            {READ UNLOCK}
} {

  do_test wal2-1.$tn.1 {
    execsql { INSERT INTO t1 VALUES($iInsert) }

    set ::locks [list]
    set ::cb_done 0

    proc tvfs_cb {method args} {
      if {$::cb_done == 0 && $method == "xShmGet"} {
        set ::cb_done 1
        if {$::wal_index_hdr_mod >= 0} {
          incr_tvfs_hdr [lindex $args 0] $::wal_index_hdr_mod 1
        }
      }

      if {$method == "xShmLock"} { lappend ::locks [lindex $args 2] }
      return SQLITE_OK
    }

    execsql { SELECT count(a), sum(a) FROM t1 } db2
  } $res

  do_test wal2-1.$tn.2 {
    set ::locks
  } $wal_locks
}
db close
db2 close
tvfs delete
file delete -force test.db test.db-wal test.db-journal

#-------------------------------------------------------------------------
# This test case is very similar to the previous one, except, after
# the reader reads the corrupt wal-index header, but before it has
# a chance to re-read it under the cover of the RECOVER lock, the
# wal-index header is replaced with a valid, but out-of-date, header.
#
# Because the header checksum looks Ok, the reader does not run recovery,
# it simply drops back to a READ lock and proceeds. But because the
# header is out-of-date, the reader reads the out-of-date snapshot.
#
# After this, the header is corrupted again and the reader is allowed
# to run recovery. This time, it sees an up-to-date snapshot of the
# database file.
#
do_test wal2-2.0 {

  testvfs tvfs tvfs_cb
  proc tvfs_cb {method args} {
    if {$method == "xShmOpen"} { set ::shm_file [lindex $args 0] }
    return SQLITE_OK
  }

  sqlite3 db  test.db -vfs tvfs
  sqlite3 db2 test.db -vfs tvfs

  execsql {
    PRAGMA journal_mode = WAL;
    CREATE TABLE t1(a);
  } db2
  execsql {
    INSERT INTO t1 VALUES(1);
    INSERT INTO t1 VALUES(2);
    INSERT INTO t1 VALUES(3);
    INSERT INTO t1 VALUES(4);
    SELECT count(a), sum(a) FROM t1;
  }
} {4 10}
do_test wal2-2.1 {
  execsql { SELECT count(a), sum(a) FROM t1 } db2
} {4 10}

foreach {tn iInsert res0 res1 wal_index_hdr_mod} {
         2    5   {4 10}   {5 15}    0
         3    6   {5 15}   {6 21}    1
         4    7   {6 21}   {7 28}    2
         5    8   {7 28}   {8 36}    3
         6    9   {8 36}   {9 45}    4
         7   10   {9 45}   {10 55}   5
         8   11   {10 55}  {11 66}   6
         9   12   {11 66}  {12 78}   7
} {
  do_test wal2-1.$tn.1 {
    set oldhdr [set_tvfs_hdr $::shm_file]
    execsql { INSERT INTO t1 VALUES($iInsert) }
    execsql { SELECT count(a), sum(a) FROM t1 }
  } $res1

  do_test wal2-2.$tn.2 {
    set ::locks [list]
    set ::cb_done 0
    proc tvfs_cb {method args} {
      if {$::cb_done == 0 && $method == "xShmGet"} {
        set ::cb_done 1
        if {$::wal_index_hdr_mod >= 0} {
          incr_tvfs_hdr $::shm_file $::wal_index_hdr_mod 1
        }
      }
      if {$method == "xShmLock"} {
        set lock [lindex $args 2]
        lappend ::locks $lock
        if {$lock == "RECOVER"} {
          set_tvfs_hdr $::shm_file $::oldhdr
        }
      }
      return SQLITE_OK
    }

    execsql { SELECT count(a), sum(a) FROM t1 } db2
  } $res0

  do_test wal2-2.$tn.3 {
    set ::locks
  } {READ RECOVER READ UNLOCK}

  do_test wal2-2.$tn.4 {
    set ::locks [list]
    set ::cb_done 0
    proc tvfs_cb {method args} {
      if {$::cb_done == 0 && $method == "xShmGet"} {
        set ::cb_done 1
        if {$::wal_index_hdr_mod >= 0} {
          incr_tvfs_hdr $::shm_file $::wal_index_hdr_mod 1
        }
      }
      if {$method == "xShmLock"} {
        set lock [lindex $args 2]
        lappend ::locks $lock
      }
      return SQLITE_OK
    }

    execsql { SELECT count(a), sum(a) FROM t1 } db2
  } $res1
}
db close
db2 close
tvfs delete
file delete -force test.db test.db-wal test.db-journal

#-------------------------------------------------------------------------
# This test case - wal2-3.* - tests the response of the library to an
# SQLITE_BUSY when attempting to obtain a READ or RECOVER lock.
#
#   wal2-3.0 - 2: SQLITE_BUSY when obtaining a READ lock
#   wal2-3.3 - 6: SQLITE_BUSY when obtaining a RECOVER lock
#
do_test wal2-3.0 {
  proc tvfs_cb {method args} {
    if {$method == "xShmLock"} {
      if {[info exists ::locked]} { return SQLITE_BUSY }
    }
    return SQLITE_OK
  }

  proc busyhandler x {
    if {$x>3} { unset -nocomplain ::locked }
    return 0
  }

  testvfs tvfs tvfs_cb
  sqlite3 db test.db -vfs tvfs
  db busy busyhandler

  execsql {
    PRAGMA journal_mode = WAL;
    CREATE TABLE t1(a);
    INSERT INTO t1 VALUES(1);
    INSERT INTO t1 VALUES(2);
    INSERT INTO t1 VALUES(3);
    INSERT INTO t1 VALUES(4);
  } 

  set ::locked 1
  info exists ::locked
} {1}
do_test wal2-3.1 {
  execsql { SELECT count(a), sum(a) FROM t1 }
} {4 10}
do_test wal2-3.2 {
  info exists ::locked
} {0}

do_test wal2-3.3 {
  proc tvfs_cb {method args} {
    if {$method == "xShmLock"} {
      if {[info exists ::sabotage]} {
        unset -nocomplain ::sabotage
        incr_tvfs_hdr [lindex $args 0] 1 1
      }
      if {[info exists ::locked] && [lindex $args 2] == "RECOVER"} {
        return SQLITE_BUSY
      }
    }
    return SQLITE_OK
  }
  set ::sabotage 1
  set ::locked 1
  list [info exists ::sabotage] [info exists ::locked]
} {1 1}
do_test wal2-3.4 {
  execsql { SELECT count(a), sum(a) FROM t1 }
} {4 10}
do_test wal2-3.5 {
  list [info exists ::sabotage] [info exists ::locked]
} {0 0}
db close
tvfs delete
file delete -force test.db test.db-wal test.db-journal

#-------------------------------------------------------------------------
# Test that a database connection using a VFS that does not support the
# xShmXXX interfaces cannot open a WAL database.
#
do_test wal2-4.1 {
  sqlite3 db test.db
  execsql {
    PRAGMA journal_mode = WAL;
    CREATE TABLE data(x);
    INSERT INTO data VALUES('need xShmOpen to see this');
    PRAGMA wal_checkpoint;
  }
} {wal}
do_test wal2-4.2 {
  db close
  proc ok {args} {return SQLITE_OK}
  testvfs -noshm tvfs ok
  sqlite3 db test.db -vfs tvfs
  catchsql { SELECT * FROM data }
} {1 {unable to open database file}}
do_test wal2-4.3 {
  db close
  proc ok {args} {return SQLITE_OK}
  testvfs tvfs ok
  sqlite3 db test.db -vfs tvfs
  catchsql { SELECT * FROM data }
} {0 {{need xShmOpen to see this}}}
db close
tvfs delete

#-------------------------------------------------------------------------
# Test that if a database connection is forced to run recovery before it
# can perform a checkpoint, it does not transition into RECOVER state.
#
do_test wal2-5.1 {
  proc tvfs_cb {method args} {
    set ::shm_file [lindex $args 0]
    if {$method == "xShmLock"} { lappend ::locks [lindex $args 2] }
    return $::tvfs_cb_return
  }
  set tvfs_cb_return SQLITE_OK

  testvfs tvfs tvfs_cb

  sqlite3 db test.db -vfs tvfs
  execsql {
    PRAGMA journal_mode = WAL;
    CREATE TABLE x(y);
    INSERT INTO x VALUES(1);
  }

  incr_tvfs_hdr $::shm_file 1 1
  set ::locks [list]
  execsql { PRAGMA wal_checkpoint }
  set ::locks
} {CHECKPOINT UNLOCK}
db close
tvfs delete

#-------------------------------------------------------------------------
# This block, test cases wal2-6.*, tests the operation of WAL with
# "PRAGMA locking_mode=EXCLUSIVE" set.
#
#   wal2-6.1.*: Changing to WAL mode before setting locking_mode=exclusive.
#
#   wal2-6.2.*: Changing to WAL mode after setting locking_mode=exclusive.
#
#   wal2-6.3.*: Changing back to rollback mode from WAL mode after setting 
#               locking_mode=exclusive.
#
#   wal2-6.4.*: Check that xShmLock calls are omitted in exclusive locking
#               mode.
#
do_test wal2-6.1.1 {
  file delete -force test.db test.db-wal test.db-journal
  sqlite3 db test.db
  execsql {
    Pragma Journal_Mode = Wal;
    Pragma Locking_Mode = Exclusive;
  }
} {wal exclusive}
do_test wal2-6.1.2 {
  execsql { PRAGMA lock_status }
} {main unlocked temp closed}
do_test wal2-6.1.3 {
  execsql {
    BEGIN;
      CREATE TABLE t1(a, b);
      INSERT INTO t1 VALUES(1, 2);
    COMMIT;
    PRAGMA lock_status;
  }
} {main exclusive temp closed}
do_test wal2-6.1.4 {
  execsql { 
    PRAGMA locking_mode = normal; 
    PRAGMA lock_status;
  }
} {normal main exclusive temp closed}
do_test wal2-6.1.5 {
  execsql { 
    SELECT * FROM t1;
    PRAGMA lock_status;
  }
} {1 2 main exclusive temp closed}
do_test wal2-6.1.6 {
  execsql {
    INSERT INTO t1 VALUES(3, 4);
    PRAGMA lock_status;
  }
} {main shared temp closed}
db close

do_test wal2-6.2.1 {
  file delete -force test.db test.db-wal test.db-journal
  sqlite3 db test.db
  execsql {
    Pragma Locking_Mode = Exclusive;
    Pragma Journal_Mode = Wal;
    Pragma Lock_Status;
  }
} {exclusive wal main exclusive temp closed}
do_test wal2-6.2.2 {
  execsql {
    BEGIN;
      CREATE TABLE t1(a, b);
      INSERT INTO t1 VALUES(1, 2);
    COMMIT;
    Pragma loCK_STATus;
  }
} {main exclusive temp closed}
do_test wal2-6.2.3 {
  db close
  sqlite3 db test.db
  execsql { PRAGMA LOCKING_MODE = EXCLUSIVE }
} {exclusive}
do_test wal2-6.2.4 {
  execsql {
    SELECT * FROM t1;
    pragma lock_status;
  }
} {1 2 main shared temp closed}
do_test wal2-6.2.5 {
  execsql {
    INSERT INTO t1 VALUES(3, 4);
    pragma lock_status;
  }
} {main exclusive temp closed}
do_test wal2-6.2.6 {
  execsql {
    PRAGMA locking_mode = NORMAL;
    pragma lock_status;
  }
} {normal main exclusive temp closed}
do_test wal2-6.2.7 {
  execsql {
    BEGIN IMMEDIATE; COMMIT;
    pragma lock_status;
  }
} {main shared temp closed}
do_test wal2-6.2.8 {
  execsql {
    PRAGMA locking_mode = EXCLUSIVE;
    BEGIN IMMEDIATE; COMMIT;
    PRAGMA locking_mode = NORMAL;
  }
  execsql {
    SELECT * FROM t1;
    pragma lock_status;
  }
} {1 2 3 4 main exclusive temp closed}
do_test wal2-6.2.9 {
  execsql {
    INSERT INTO t1 VALUES(5, 6);
    SELECT * FROM t1;
    pragma lock_status;
  }
} {1 2 3 4 5 6 main shared temp closed}
db close

do_test wal2-6.3.1 {
  file delete -force test.db test.db-wal test.db-journal
  sqlite3 db test.db
  execsql {
    PRAGMA journal_mode = WAL;
    PRAGMA locking_mode = exclusive;
    BEGIN;
      CREATE TABLE t1(x);
      INSERT INTO t1 VALUES('Chico');
      INSERT INTO t1 VALUES('Harpo');
    COMMIT;
  }
  list [file exists test.db-wal] [file exists test.db-journal]
} {1 0}
do_test wal2-6.3.2 {
  execsql { PRAGMA journal_mode = DELETE }
  file exists test.db-wal
} {0}
do_test wal2-6.3.3 {
  execsql { PRAGMA lock_status }
} {main exclusive temp closed}
do_test wal2-6.3.4 {
  execsql { 
    BEGIN;
      INSERT INTO t1 VALUES('Groucho');
  }
  list [file exists test.db-wal] [file exists test.db-journal]
} {0 1}
do_test wal2-6.3.5 {
  execsql { PRAGMA lock_status }
} {main exclusive temp closed}
do_test wal2-6.3.6 {
  execsql { COMMIT }
  list [file exists test.db-wal] [file exists test.db-journal]
} {0 1}
do_test wal2-6.3.7 {
  execsql { PRAGMA lock_status }
} {main exclusive temp closed}
db close

do_test wal2-6.4.1 {
  file delete -force test.db test.db-wal test.db-journal
  proc tvfs_cb {method args} {
    set ::shm_file [lindex $args 0]
    if {$method == "xShmLock"} { lappend ::locks [lindex $args 2] }
    return "SQLITE_OK"
  }
  testvfs tvfs tvfs_cb
  sqlite3 db test.db -vfs tvfs

  execsql {
    PRAGMA journal_mode = WAL;
    CREATE TABLE t1(x);
    INSERT INTO t1 VALUES('Leonard');
    INSERT INTO t1 VALUES('Arthur');
  }

  set ::locks [list]
  execsql { PRAGMA locking_mode = exclusive }
  set ::locks
} {}
do_test wal2-6.4.2 {
  execsql { SELECT * FROM t1 }
} {Leonard Arthur}
do_test wal2-6.4.3 {
  set ::locks
} {READ}
do_test wal2-6.4.4 {
  execsql { 
    INSERT INTO t1 VALUES('Julius Henry');
    SELECT * FROM t1;
  }
} {Leonard Arthur {Julius Henry}}
do_test wal2-6.4.5 {
  set ::locks
} {READ}
do_test wal2-6.4.6 {
  execsql {
    PRAGMA locking_mode = NORMAL;
    DELETE FROM t1;
  }
  set ::locks
} {READ UNLOCK}
do_test wal2-6.4.7 {
  set ::locks [list]
  execsql { INSERT INTO t1 VALUES('Karl') }
  set ::locks
} {READ WRITE READ UNLOCK}
db close
tvfs delete

do_test wal2-6.5.1 {
  sqlite3 db test.db
  execsql {
    PRAGMA journal_mode = wal;
    PRAGMA locking_mode = exclusive;
    CREATE TABLE t2(a, b);
    PRAGMA wal_checkpoint;
    INSERT INTO t2 VALUES('I', 'II');
    PRAGMA journal_mode;
  }
} {wal exclusive wal}
do_test wal2-6.5.2 {
  execsql {
    PRAGMA locking_mode = normal;
    INSERT INTO t2 VALUES('III', 'IV');
    PRAGMA locking_mode = exclusive;
    SELECT * FROM t2;
  }
} {normal exclusive I II III IV}
do_test wal2-6.5.3 {
  execsql { PRAGMA wal_checkpoint }
} {}

db close

finish_test

