Add an alternative application-defined pcache implementation and add test
cases to permutations.test to invoke it.  Added the SQLITE_CONFIG_GETPCACHE
method to sqlite3_config(). (CVS 5920)

FossilOrigin-Name: 16f1e6ec2ad92f68c0079a0c2b5ca08a3b4af816
diff --git a/src/main.c b/src/main.c
index 3662697..16f89b7 100644
--- a/src/main.c
+++ b/src/main.c
@@ -14,7 +14,7 @@
 ** other files are for internal use by SQLite and should not be
 ** accessed by users of the library.
 **
-** $Id: main.c,v 1.512 2008/11/13 14:28:29 danielk1977 Exp $
+** $Id: main.c,v 1.513 2008/11/19 01:20:26 drh Exp $
 */
 #include "sqliteInt.h"
 #include <ctype.h>
@@ -315,6 +315,14 @@
       break;
     }
 
+    case SQLITE_CONFIG_GETPCACHE: {
+      if( sqlite3GlobalConfig.pcache.xInit==0 ){
+        sqlite3PCacheSetDefault();
+      }
+      *va_arg(ap, sqlite3_pcache_methods*) = sqlite3GlobalConfig.pcache;
+      break;
+    }
+
 #if defined(SQLITE_ENABLE_MEMSYS3) || defined(SQLITE_ENABLE_MEMSYS5)
     case SQLITE_CONFIG_HEAP: {
       /* Designate a buffer for heap memory space */
diff --git a/src/sqlite.h.in b/src/sqlite.h.in
index 39070f7..8f1c734 100644
--- a/src/sqlite.h.in
+++ b/src/sqlite.h.in
@@ -30,7 +30,7 @@
 ** the version number) and changes its name to "sqlite3.h" as
 ** part of the build process.
 **
-** @(#) $Id: sqlite.h.in,v 1.414 2008/11/18 19:18:09 drh Exp $
+** @(#) $Id: sqlite.h.in,v 1.415 2008/11/19 01:20:26 drh Exp $
 */
 #ifndef _SQLITE3_H_
 #define _SQLITE3_H_
@@ -6683,8 +6683,9 @@
 **
 ** The xRekey() method is used to change the key value associated with the
 ** page passed as the second argument from oldKey to newKey. If the cache
-** contains an entry associated with oldKey, it should be discarded. Any
-** cache entry associated with oldKey is guaranteed not to be pinned.
+** previously contains an entry associated with newKey, it should be
+** discarded. Any prior cache entry associated with newKey is guaranteed not
+** to be pinned.
 **
 ** When SQLite calls the xTruncate() method, the cache must discard all
 ** existing cache entries with page numbers (keys) greater than or equal
diff --git a/src/test_malloc.c b/src/test_malloc.c
index bcf18d2..2009f54 100644
--- a/src/test_malloc.c
+++ b/src/test_malloc.c
@@ -13,7 +13,7 @@
 ** This file contains code used to implement test interfaces to the
 ** memory allocation subsystem.
 **
-** $Id: test_malloc.c,v 1.50 2008/11/10 18:05:36 shane Exp $
+** $Id: test_malloc.c,v 1.51 2008/11/19 01:20:26 drh Exp $
 */
 #include "sqliteInt.h"
 #include "tcl.h"
@@ -946,6 +946,42 @@
 }
 
 /*
+** Usage:    sqlite3_config_alt_pcache INSTALL_FLAG DISCARD_CHANCE PRNG_SEED
+**
+** Set up the alternative test page cache.  Install if INSTALL_FLAG is
+** true and uninstall (reverting to the default page cache) if INSTALL_FLAG
+** is false.  DISCARD_CHANGE is an integer between 0 and 100 inclusive
+** which determines the chance of discarding a page when unpinned.  100
+** is certainty.  0 is never.  PRNG_SEED is the pseudo-random number generator
+** seed.
+*/
+static int test_alt_pcache(
+  void * clientData,
+  Tcl_Interp *interp,
+  int objc,
+  Tcl_Obj *CONST objv[]
+){
+  int installFlag;
+  int discardChance;
+  int prngSeed;
+  extern void installTestPCache(int,unsigned,unsigned);
+  if( objc!=4 ){
+    Tcl_WrongNumArgs(interp, 1, objv, "INSTALLFLAG DISCARDCHANCE PRNGSEEED");
+    return TCL_ERROR;
+  }
+  if( Tcl_GetIntFromObj(interp, objv[1], &installFlag) ) return TCL_ERROR;
+  if( Tcl_GetIntFromObj(interp, objv[2], &discardChance) ) return TCL_ERROR;
+  if( Tcl_GetIntFromObj(interp, objv[3], &prngSeed) ) return TCL_ERROR;
+  if( discardChance<0 || discardChance>100 ){
+    Tcl_AppendResult(interp, "discard-chance should be between 0 and 100",
+                     (char*)0);
+    return TCL_ERROR;
+  }
+  installTestPCache(installFlag, (unsigned)discardChance, (unsigned)prngSeed);
+  return TCL_OK;
+}
+
+/*
 ** Usage:    sqlite3_config_memstatus BOOLEAN
 **
 ** Enable or disable memory status reporting using SQLITE_CONFIG_MEMSTATUS.
@@ -1312,6 +1348,7 @@
      { "sqlite3_memdebug_log",       test_memdebug_log             ,0 },
      { "sqlite3_config_scratch",     test_config_scratch           ,0 },
      { "sqlite3_config_pagecache",   test_config_pagecache         ,0 },
+     { "sqlite3_config_alt_pcache",  test_alt_pcache               ,0 },
      { "sqlite3_status",             test_status                   ,0 },
      { "sqlite3_db_status",          test_db_status                ,0 },
      { "install_malloc_faultsim",    test_install_malloc_faultsim  ,0 },
@@ -1321,7 +1358,7 @@
      { "sqlite3_config_error",       test_config_error             ,0 },
      { "sqlite3_db_config_lookaside",test_db_config_lookaside      ,0 },
      { "sqlite3_dump_memsys3",       test_dump_memsys3             ,3 },
-     { "sqlite3_dump_memsys5",       test_dump_memsys3             ,5 }
+     { "sqlite3_dump_memsys5",       test_dump_memsys3             ,5 },
   };
   int i;
   for(i=0; i<sizeof(aObjCmd)/sizeof(aObjCmd[0]); i++){
diff --git a/src/test_pcache.c b/src/test_pcache.c
new file mode 100644
index 0000000..58f5b39
--- /dev/null
+++ b/src/test_pcache.c
@@ -0,0 +1,438 @@
+/*
+** 2008 November 18
+**
+** 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 contains code used for testing the SQLite system.
+** None of the code in this file goes into a deliverable build.
+** 
+** This file contains an application-defined pager cache
+** implementation that can be plugged in in place of the
+** default pcache.  This alternative pager cache will throw
+** some errors that the default cache does not.
+**
+** This pagecache implementation is designed for simplicity
+** not speed.  
+**
+** $Id: test_pcache.c,v 1.1 2008/11/19 01:20:26 drh Exp $
+*/
+#include "sqlite3.h"
+#include <string.h>
+#include <assert.h>
+
+/*
+** Global data used by this test implementation.  There is no
+** mutexing, which means this page cache will not work in a
+** multi-threaded test.
+*/
+typedef struct testpcacheGlobalType testpcacheGlobalType;
+struct testpcacheGlobalType {
+  void *pDummy;             /* Dummy allocation to simulate failures */
+  int nInstance;            /* Number of current instances */
+  unsigned discardChance;   /* Chance of discarding on an unpin */
+  unsigned prngSeed;        /* Seed for the PRNG */
+};
+static testpcacheGlobalType testpcacheGlobal;
+
+/*
+** Initializer.
+**
+** Verify that the initializer is only called when the system is
+** uninitialized.  Allocate some memory and report SQLITE_NOMEM if
+** the allocation fails.  This provides a means to test the recovery
+** from a failed initialization attempt.  It also verifies that the
+** the destructor always gets call - otherwise there would be a
+** memory leak.
+*/
+static int testpcacheInit(void *pArg){
+  assert( pArg==(void*)&testpcacheGlobal );
+  assert( testpcacheGlobal.pDummy==0 );
+  assert( testpcacheGlobal.nInstance==0 );
+  testpcacheGlobal.pDummy = sqlite3_malloc(10);
+  return testpcacheGlobal.pDummy==0 ? SQLITE_NOMEM : SQLITE_OK;
+}
+
+/*
+** Destructor
+**
+** Verify that this is only called after initialization.
+** Free the memory allocated by the initializer.
+*/
+static void testpcacheShutdown(void *pArg){
+  assert( pArg==(void*)&testpcacheGlobal );
+  assert( testpcacheGlobal.pDummy!=0 );
+  assert( testpcacheGlobal.nInstance==0 );
+  sqlite3_free( testpcacheGlobal.pDummy );
+  testpcacheGlobal.pDummy = 0;
+}
+
+/*
+** Number of pages in a cache
+*/
+#define TESTPCACHE_NPAGE    217
+#define TESTPCACHE_RESERVE   17
+
+/*
+** Magic numbers used to determine validity of the page cache.
+*/
+#define TESTPCACHE_VALID  0x364585fd
+#define TESTPCACHE_CLEAR  0xd42670d4
+
+/*
+** Private implementation of a page cache.
+*/
+typedef struct testpcache testpcache;
+struct testpcache {
+  int szPage;               /* Size of each page.  Multiple of 8. */
+  int bPurgeable;           /* True if the page cache is purgeable */
+  int nFree;                /* Number of unused slots in a[] */
+  int nPinned;              /* Number of pinned slots in a[] */
+  unsigned iRand;           /* State of the PRNG */
+  unsigned iMagic;          /* Magic number for sanity checking */
+  struct testpcachePage {
+    unsigned key;              /* The key for this page. 0 means unallocated */
+    int isPinned;              /* True if the page is pinned */
+    void *pData;               /* Data for this page */
+  } a[TESTPCACHE_NPAGE];    /* All pages in the cache */
+};
+
+/*
+** Get a random number using the PRNG in the given page cache.
+*/
+static unsigned testpcacheRandom(testpcache *p){
+  unsigned x = 0;
+  int i;
+  for(i=0; i<4; i++){
+    p->iRand = (p->iRand*69069 + 5);
+    x = (x<<8) | ((p->iRand>>16)&0xff);
+  }
+  return x;
+}
+
+
+/*
+** Allocate a new page cache instance.
+*/
+static sqlite3_pcache *testpcacheCreate(int szPage, int bPurgeable){
+  int nMem;
+  char *x;
+  testpcache *p;
+  int i;
+  assert( testpcacheGlobal.pDummy!=0 );
+  szPage = (szPage+7)&~7;
+  nMem = sizeof(testpcache) + TESTPCACHE_NPAGE*szPage;
+  p = sqlite3_malloc( nMem );
+  if( p==0 ) return 0;
+  x = (char*)&p[1];
+  p->szPage = szPage;
+  p->nFree = TESTPCACHE_NPAGE;
+  p->nPinned = 0;
+  p->iRand = testpcacheGlobal.prngSeed;
+  p->bPurgeable = bPurgeable;
+  p->iMagic = TESTPCACHE_VALID;
+  for(i=0; i<TESTPCACHE_NPAGE; i++, x += szPage){
+    p->a[i].key = 0;
+    p->a[i].isPinned = 0;
+    p->a[i].pData = (void*)x;
+  }
+  testpcacheGlobal.nInstance++;
+  return (sqlite3_pcache*)p;
+}
+
+/*
+** Set the cache size
+*/
+static void testpcacheCachesize(sqlite3_pcache *pCache, int newSize){
+  testpcache *p = (testpcache*)pCache;
+  assert( p->iMagic==TESTPCACHE_VALID );
+  assert( newSize>=1 );
+  assert( testpcacheGlobal.pDummy!=0 );
+  assert( testpcacheGlobal.nInstance>0 );
+}
+
+/*
+** Return the number of pages in the cache that are being used.
+** This includes both pinned and unpinned pages.
+*/
+static int testpcachePagecount(sqlite3_pcache *pCache){
+  testpcache *p = (testpcache*)pCache;
+  assert( p->iMagic==TESTPCACHE_VALID );
+  assert( testpcacheGlobal.pDummy!=0 );
+  assert( testpcacheGlobal.nInstance>0 );
+  return TESTPCACHE_NPAGE - p->nFree;
+}
+
+/*
+** Fetch a page.
+*/
+static void *testpcacheFetch(
+  sqlite3_pcache *pCache,
+  unsigned key,
+  int createFlag
+){
+  testpcache *p = (testpcache*)pCache;
+  int i, j;
+  assert( p->iMagic==TESTPCACHE_VALID );
+  assert( testpcacheGlobal.pDummy!=0 );
+  assert( testpcacheGlobal.nInstance>0 );
+
+  /* See if the page is already in cache.  Return immediately if it is */
+  for(i=0; i<TESTPCACHE_NPAGE; i++){
+    if( p->a[i].key==key ){
+      if( !p->a[i].isPinned ){
+        p->nPinned++;
+        assert( p->nPinned <= TESTPCACHE_NPAGE - p->nFree );
+        p->a[i].isPinned = 1;
+      }
+      return p->a[i].pData;
+    }
+  }
+
+  /* If createFlag is 0, never allocate a new page */
+  if( createFlag==0 ){
+    return 0;
+  }
+
+  /* If no pages are available, always fail */
+  if( p->nPinned==TESTPCACHE_NPAGE ){
+    return 0;
+  }
+
+  /* Do not allocate the last TESTPCACHE_RESERVE pages unless createFlag is 2 */
+  if( p->nPinned>=TESTPCACHE_NPAGE-TESTPCACHE_RESERVE && createFlag<2 ){
+    return 0;
+  }
+
+  /* Find a free page to allocate if there are any free pages.
+  ** Withhold TESTPCACHE_RESERVE free pages until createFlag is 2.
+  */
+  if( p->nFree>TESTPCACHE_RESERVE || (createFlag==2 && p->nFree>0) ){
+    j = testpcacheRandom(p) % TESTPCACHE_NPAGE;
+    for(i=0; i<TESTPCACHE_NPAGE; i++, j = (j+1)%TESTPCACHE_NPAGE){
+      if( p->a[j].key==0 ){
+        p->a[j].key = key;
+        p->a[j].isPinned = 1;
+        memset(p->a[j].pData, 0, p->szPage);
+        p->nPinned++;
+        p->nFree--;
+        assert( p->nPinned <= TESTPCACHE_NPAGE - p->nFree );
+        return p->a[j].pData;
+      }
+    }
+
+    /* The prior loop always finds a freepage to allocate */
+    assert( 0 );
+  }
+
+  /* If this cache is not purgeable then we have to fail.
+  */
+  if( p->bPurgeable==0 ){
+    return 0;
+  }
+
+  /* If there are no free pages, recycle a page.  The page to
+  ** recycle is selected at random from all unpinned pages.
+  */
+  j = testpcacheRandom(p) % TESTPCACHE_NPAGE;
+  for(i=0; i<TESTPCACHE_NPAGE; i++, j = (j+1)%TESTPCACHE_NPAGE){
+    if( p->a[j].key>0 && p->a[j].isPinned==0 ){
+      p->a[j].key = key;
+      p->a[j].isPinned = 1;
+      memset(p->a[j].pData, 0, p->szPage);
+      p->nPinned++;
+      assert( p->nPinned <= TESTPCACHE_NPAGE - p->nFree );
+      return p->a[j].pData;
+    }
+  }
+
+  /* The previous loop always finds a page to recycle. */
+  assert(0);
+  return 0;
+}
+
+/*
+** Unpin a page.
+*/
+static void testpcacheUnpin(
+  sqlite3_pcache *pCache,
+  void *pOldPage,
+  int discard
+){
+  testpcache *p = (testpcache*)pCache;
+  int i;
+  assert( p->iMagic==TESTPCACHE_VALID );
+  assert( testpcacheGlobal.pDummy!=0 );
+  assert( testpcacheGlobal.nInstance>0 );
+
+  /* Randomly discard pages as they are unpinned according to the
+  ** discardChance setting.  If discardChance is 0, the random discard
+  ** never happens.  If discardChance is 100, it always happens.
+  */
+  if( p->bPurgeable
+  && (100-testpcacheGlobal.discardChance) <= (testpcacheRandom(p)%100)
+  ){
+    discard = 1;
+  }
+
+  for(i=0; i<TESTPCACHE_NPAGE; i++){
+    if( p->a[i].pData==pOldPage ){
+      /* The pOldPage pointer always points to a pinned page */
+      assert( p->a[i].isPinned );
+      p->a[i].isPinned = 0;
+      p->nPinned--;
+      assert( p->nPinned>=0 );
+      if( discard ){
+        p->a[i].key = 0;
+        p->nFree++;
+        assert( p->nFree<=TESTPCACHE_NPAGE );
+      }
+      return;
+    }
+  }
+
+  /* The pOldPage pointer always points to a valid page */
+  assert( 0 );
+}
+
+
+/*
+** Rekey a single page.
+*/
+static void testpcacheRekey(
+  sqlite3_pcache *pCache,
+  void *pOldPage,
+  unsigned oldKey,
+  unsigned newKey
+){
+  testpcache *p = (testpcache*)pCache;
+  int i;
+  assert( p->iMagic==TESTPCACHE_VALID );
+  assert( testpcacheGlobal.pDummy!=0 );
+  assert( testpcacheGlobal.nInstance>0 );
+
+  /* If there already exists another page at newKey, verify that
+  ** the other page is unpinned and discard it.
+  */
+  for(i=0; i<TESTPCACHE_NPAGE; i++){
+    if( p->a[i].key==newKey ){
+      /* The new key is never a page that is already pinned */
+      assert( p->a[i].isPinned==0 );
+      p->a[i].key = 0;
+      p->nFree++;
+      assert( p->nFree<=TESTPCACHE_NPAGE );
+      break;
+    }
+  }
+
+  /* Find the page to be rekeyed and rekey it.
+  */
+  for(i=0; i<TESTPCACHE_NPAGE; i++){
+    if( p->a[i].key==oldKey ){
+      /* The oldKey and pOldPage parameters match */
+      assert( p->a[i].pData==pOldPage );
+      /* Page to be rekeyed must be pinned */
+      assert( p->a[i].isPinned );
+      p->a[i].key = newKey;
+      return;
+    }
+  }
+
+  /* Rekey is always given a valid page to work with */
+  assert( 0 );
+}
+
+
+/*
+** Truncate the page cache.  Every page with a key of iLimit or larger
+** is discarded.
+*/
+static void testpcacheTruncate(sqlite3_pcache *pCache, unsigned iLimit){
+  testpcache *p = (testpcache*)pCache;
+  unsigned int i;
+  assert( p->iMagic==TESTPCACHE_VALID );
+  assert( testpcacheGlobal.pDummy!=0 );
+  assert( testpcacheGlobal.nInstance>0 );
+  for(i=0; i<TESTPCACHE_NPAGE; i++){
+    if( p->a[i].key>=iLimit ){
+      p->a[i].key = 0;
+      if( p->a[i].isPinned ){
+        p->nPinned--;
+        assert( p->nPinned>=0 );
+      }
+      p->nFree++;
+      assert( p->nFree<=TESTPCACHE_NPAGE );
+    }
+  }
+}
+
+/*
+** Destroy a page cache.
+*/
+static void testpcacheDestroy(sqlite3_pcache *pCache){
+  testpcache *p = (testpcache*)pCache;
+  assert( p->iMagic==TESTPCACHE_VALID );
+  assert( testpcacheGlobal.pDummy!=0 );
+  assert( testpcacheGlobal.nInstance>0 );
+  p->iMagic = TESTPCACHE_CLEAR;
+  sqlite3_free(p);
+  testpcacheGlobal.nInstance--;
+}
+
+
+/*
+** Invoke this routine to register or unregister the testing pager cache
+** implemented by this file.
+**
+** Install the test pager cache if installFlag is 1 and uninstall it if
+** installFlag is 0.
+**
+** When installing, discardChance is a number between 0 and 100 that
+** indicates the probability of discarding a page when unpinning the
+** page.  0 means never discard (unless the discard flag is set).
+** 100 means always discard.
+*/
+void installTestPCache(
+  int installFlag,            /* True to install.  False to uninstall. */
+  unsigned discardChance,     /* 0-100.  Chance to discard on unpin */
+  unsigned prngSeed           /* Seed for the PRNG */
+){
+  static const sqlite3_pcache_methods testPcache = {
+    (void*)&testpcacheGlobal,
+    testpcacheInit,
+    testpcacheShutdown,
+    testpcacheCreate,
+    testpcacheCachesize,
+    testpcachePagecount,
+    testpcacheFetch,
+    testpcacheUnpin,
+    testpcacheRekey,
+    testpcacheTruncate,
+    testpcacheDestroy,
+  };
+  static sqlite3_pcache_methods defaultPcache;
+  static int isInstalled = 0;
+
+  assert( testpcacheGlobal.nInstance==0 );
+  assert( testpcacheGlobal.pDummy==0 );
+  assert( discardChance<=100 );
+  testpcacheGlobal.discardChance = discardChance;
+  testpcacheGlobal.prngSeed = prngSeed ^ (prngSeed<<16);
+  if( installFlag!=isInstalled ){
+    if( installFlag ){
+      sqlite3_config(SQLITE_CONFIG_GETPCACHE, &defaultPcache);
+      assert( defaultPcache.xCreate!=testpcacheCreate );
+      sqlite3_config(SQLITE_CONFIG_PCACHE, &testPcache);
+    }else{
+      assert( defaultPcache.xCreate!=0 );
+      sqlite3_config(SQLITE_CONFIG_PCACHE, &defaultPcache);
+    }
+    isInstalled = installFlag;
+  }
+}