# 2018 December 23
#
# 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.
#
# TESTRUNNER: slow

set testdir [file dirname $argv0]
source $testdir/tester.tcl
source $testdir/lock_common.tcl
source $testdir/malloc_common.tcl
source $testdir/wal_common.tcl
set testprefix walvfs

ifcapable !wal {finish_test ; return }

db close
testvfs tvfs 
tvfs script xSync
tvfs filter xSync
set ::sync_count 0
proc xSync {method file args} {
  if {[file tail $file]=="test.db-wal"} {
    incr ::sync_count
  }
}


#-------------------------------------------------------------------------
# Test that if IOCAP_SEQUENTIAL is set, the wal-header is not synced to
# disk immediately after it is written.
#
sqlite3 db test.db -vfs tvfs
do_execsql_test 1.0 {
  PRAGMA auto_vacuum = 0;
  PRAGMA journal_mode = wal;
  PRAGMA synchronous = normal;
  CREATE TABLE t1(a, b, c);
  INSERT INTO t1 VALUES(1, 2, 3);
  INSERT INTO t1 VALUES(4, 5, 6);
  INSERT INTO t1 VALUES(7, 8, 9);
  PRAGMA wal_checkpoint;
} {wal 0 5 5}

set ::sync_count 0
do_test 1.1 {
  execsql { INSERT INTO t1 VALUES(10, 11, 12) }
  set ::sync_count
} 1

db close
tvfs devchar sequential
sqlite3 db test.db -vfs tvfs
do_execsql_test 1.2 {
  PRAGMA synchronous = normal;
  INSERT INTO t1 VALUES(13, 14, 15);
  INSERT INTO t1 VALUES(16, 17, 18);
  PRAGMA wal_checkpoint;
} {0 4 4}

set ::sync_count 0
do_test 1.3 {
  execsql { INSERT INTO t1 VALUES(10, 11, 12) }
  set ::sync_count
} 0

#-------------------------------------------------------------------------
# Test that "PRAGMA journal_size_limit" works in wal mode.
#
reset_db
do_execsql_test 2.0 {
  PRAGMA journal_size_limit = 10000;
  CREATE TABLE t1(x);
  PRAGMA journal_mode = wal;
  WITH s(i) AS (
    SELECT 1 UNION ALL SELECT i+1 FROM s LIMIT 20
  )
  INSERT INTO t1 SELECT randomblob(750) FROM s;
} {10000 wal}
do_test 2.1 {
  expr [file size test.db-wal]>12000
} {1}
do_test 2.2 {
  execsql {
    PRAGMA wal_checkpoint;
    INSERT INTO t1 VALUES(randomblob(750));
  }
  file size test.db-wal
} {10000}
do_test 2.3 {
  execsql {
    PRAGMA journal_size_limit = 8000;
    PRAGMA wal_checkpoint;
    INSERT INTO t1 VALUES(randomblob(750));
  }
  file size test.db-wal
} {8000}

#-------------------------------------------------------------------------
# Test that a checkpoint may be interrupted using sqlite3_interrupt().
# And that the error code is SQLITE_NOMEM, not SQLITE_INTERRUPT, if
# an OOM error occurs just before the sqlite3_interrupt() call.
#
reset_db
db close
sqlite3 db test.db -vfs tvfs
tvfs filter {}

do_execsql_test 3.0 {
  CREATE TABLE t1(x);
  PRAGMA journal_mode = wal;
  WITH s(i) AS (
    SELECT 1 UNION ALL SELECT i+1 FROM s LIMIT 20
  )
  INSERT INTO t1 SELECT randomblob(750) FROM s;
} {wal}

tvfs filter xWrite
tvfs script xWrite
set ::cnt 2
proc xWrite {method file args} {
  if {[file tail $file]=="test.db"} {
    incr ::cnt -1
    if {$::cnt==0} {
      sqlite3_interrupt db
    }
  }
  return SQLITE_OK
}

do_catchsql_test 3.1 {
  PRAGMA wal_checkpoint
} {1 interrupted}

set ::cnt 2
proc xWrite {method file args} {
  if {[file tail $file]=="test.db"} {
    incr ::cnt -1
    if {$::cnt==0} {
      sqlite3_memdebug_fail 1 -repeat 0
      # For this test to pass, the following statement must call malloc() at 
      # least once. Even if the lookaside is enabled.
      set ::xwrite_stmt_res [catchsql { SELECT hex(randomblob(4000)) }]
      sqlite3_interrupt db
    }
  }
  return SQLITE_OK
}

set ::xwrite_stmt_res ""
do_catchsql_test 3.2 {
  PRAGMA wal_checkpoint
} {1 {out of memory}}
do_test 3.2.2 {
  set ::xwrite_stmt_res
} {1 {out of memory}}
unset ::xwrite_stmt_res

#-------------------------------------------------------------------------
#
reset_db
db close
do_test 4.0 {
  sqlite3 db test.db -vfs tvfs
  execsql {
    CREATE TABLE t1(x);
    PRAGMA journal_mode = wal;
    WITH s(i) AS (
        SELECT 1 UNION ALL SELECT i+1 FROM s LIMIT 20
    )
    INSERT INTO t1 SELECT randomblob(750) FROM s;
  } db
} {wal}
db close

tvfs filter xShmMap
tvfs script xShmMap
proc xShmMap {method file args} { 
  return SQLITE_READONLY 
}
sqlite3 db test.db -vfs tvfs
do_catchsql_test 4.1 {
  SELECT count(*) FROM t1
} {1 {attempt to write a readonly database}}

set ::cnt 5
tvfs filter {xShmMap xShmLock}
proc xShmMap {method file name args} { 
  switch -- $method {
    xShmMap {  return SQLITE_READONLY }
    xShmLock {
      if {$args == "{0 1 lock shared}"} {
        incr ::cnt -1
        if {$::cnt>0} { return SQLITE_BUSY }
      }
    }
  }
  return SQLITE_OK
}
do_catchsql_test 4.2 {
  SELECT count(*) FROM t1
} {1 {attempt to write a readonly database}}

#-------------------------------------------------------------------------
#
reset_db
db close 
sqlite3 db test.db -vfs tvfs
tvfs filter {}
do_execsql_test 5.0 {
  PRAGMA auto_vacuum = 0;
  PRAGMA page_size = 1024;
  CREATE TABLE t1(x);
  PRAGMA journal_mode = wal;
  WITH s(i) AS (
      SELECT 1 UNION ALL SELECT i+1 FROM s LIMIT 20
  )
  INSERT INTO t1 SELECT randomblob(750) FROM s;
} {wal}

do_execsql_test 5.1 {
  SELECT count(*) FROM t1
} {20}

do_test 5.2 {
  vfs_set_readmark db main 1 100
  vfs_set_readmark db main 2 100
  vfs_set_readmark db main 3 100
  vfs_set_readmark db main 4 100
} {100}

do_execsql_test 5.3 {
  SELECT count(*) FROM t1
} {20}

do_test 5.3 {
  list [vfs_set_readmark db main 1] \
       [vfs_set_readmark db main 2] \
       [vfs_set_readmark db main 3] \
       [vfs_set_readmark db main 4] 
} {24 100 100 100}

tvfs script xShmLock
tvfs filter xShmLock
set ::cnt 20
proc xShmLock {args} {
  incr ::cnt -1
  if {$::cnt>0} { return SQLITE_BUSY }
  return SQLITE_OK
}

do_test 5.4 {
  vfs_set_readmark db main 1 100
  execsql { SELECT count(*) FROM t1 }
} {20}

vfs_set_readmark db main 1 100
vfs_set_readmark db main 2 100
vfs_set_readmark db main 3 100
vfs_set_readmark db main 4 100

tvfs script xShmMapLock
tvfs filter {xShmLock xShmMap}
proc xShmMapLock {method args} {
  if {$method=="xShmMap"} {
    return "SQLITE_READONLY"
  }
  return SQLITE_BUSY
}

sqlite3 db2 test.db -vfs tvfs
breakpoint
do_test 5.5 {
  list [catch { execsql { SELECT count(*) FROM t1 } db2 } msg] $msg
} {1 {attempt to write a readonly database}}

tvfs filter {}
vfs_set_readmark db main 1 1

do_test 5.6 {
  list [catch { execsql { SELECT count(*) FROM t1 } db2 } msg] $msg
} {0 20}
db2 close
db close

#-------------------------------------------------------------------------
# Cause an SQLITE_PROTOCOL while attempting to restart the wal file.
#
reset_db
tvfs filter {}
db close
sqlite3 db test.db -vfs tvfs
do_execsql_test 6.0 {
  PRAGMA auto_vacuum = 0;
  PRAGMA page_size = 1024;
  CREATE TABLE t1(x);
  PRAGMA journal_mode = wal;
  WITH s(i) AS (
      SELECT 1 UNION ALL SELECT i+1 FROM s LIMIT 20
  )
  INSERT INTO t1 SELECT randomblob(750) FROM s;
} {wal}

do_test 6.1 {
  execsql { PRAGMA wal_checkpoint } 
  set {} {}
} {}

tvfs filter xShmLock
tvfs script xShmLock
set ::flag 0
proc xShmLock {method file handle spec} {
  if {$::flag && [lrange $spec 2 end]=="lock shared"} {
    return SQLITE_BUSY
  }
  if {$spec=="3 1 unlock shared"} {
    set ::flag 1
  }
  return SQLITE_OK
}

puts "# WARNING: This next test takes around 12 seconds"
do_catchsql_test 6.2 {
  INSERT INTO t1 VALUES(1);
} {1 {locking protocol}}

#-------------------------------------------------------------------------
# Check that a checkpoint fails if it cannot get the CHECKPOINTER lock
#
reset_db
tvfs filter {}
db close
sqlite3 db test.db -vfs tvfs
do_execsql_test 7.0 {
  PRAGMA auto_vacuum = 0;
  PRAGMA page_size = 1024;
  CREATE TABLE t1(x);
  PRAGMA journal_mode = wal;
  WITH s(i) AS (
      SELECT 1 UNION ALL SELECT i+1 FROM s LIMIT 20
  )
  INSERT INTO t1 SELECT randomblob(750) FROM s;
} {wal}

tvfs script xShmLock
tvfs filter xShmLock
proc xShmLock {method file handle spec} {
  if {$spec=="1 1 lock exclusive"} {
    return SQLITE_BUSY
  }
  return SQLITE_OK
}

do_execsql_test 7.1 {
  PRAGMA wal_checkpoint
} {1 -1 -1}

#-------------------------------------------------------------------------
# Check that the page cache is correctly flushed if a checkpointer using
# a version 2 VFS makes a checkpoint with an out-of-date cache.
#
reset_db
testvfs tvfs2 -iversion 2
db close
sqlite3 db test.db -vfs tvfs2
do_execsql_test 8.0 {
  PRAGMA auto_vacuum = 0;
  PRAGMA page_size = 1024;
  CREATE TABLE t1(x);
  PRAGMA journal_mode = wal;
  WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s LIMIT 20 )
  INSERT INTO t1 SELECT randomblob(75) FROM s;
} {wal}

do_execsql_test 8.1 { SELECT count(*) FROM t1 } {20}

do_test 8.2 {
  sqlite3 db2 test.db -vfs tvfs2
  execsql {
    INSERT INTO t1 VALUES(randomblob(75));
  } db2
  db2 close
} {}

do_execsql_test 8.3 { 
  PRAGMA wal_checkpoint;
  SELECT count(*) FROM t1 
} {0 5 5 21}
db close
tvfs2 delete

#-------------------------------------------------------------------------
reset_db
db close
sqlite3 db test.db -vfs tvfs
do_execsql_test 9.0 {
  PRAGMA auto_vacuum = 0;
  PRAGMA page_size = 1024;
  CREATE TABLE t1(x);
  PRAGMA journal_mode = wal;
  WITH s(i) AS ( SELECT 1 UNION ALL SELECT i+1 FROM s LIMIT 20 )
  INSERT INTO t1 SELECT randomblob(75) FROM s;
} {wal}

sqlite3 db2 test.db -vfs tvfs
tvfs filter {xShmMap xShmLock}
tvfs script xShmMap
proc xShmMap {method file handle args} {
  switch -- $method {
    xShmMap {
      return "SQLITE_READONLY_CANTINIT"
    }
    xShmLock {
      if {$args=="{3 1 lock shared}"} {
        return "SQLITE_IOERR"
      }
    }
  }
}

do_test 9.1 {
  catchsql { SELECT count(*) FROM t1 } db2
} {1 {disk I/O error}}

db close
db2 close
tvfs delete
finish_test