]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Add an experimental "BEGIN UNLOCKED" command.
authordan <dan@noemail.net>
Mon, 27 Jul 2015 19:31:45 +0000 (19:31 +0000)
committerdan <dan@noemail.net>
Mon, 27 Jul 2015 19:31:45 +0000 (19:31 +0000)
FossilOrigin-Name: 8079421604dbd40d03471dad6d12115119b554c2

17 files changed:
manifest
manifest.uuid
src/btree.c
src/btree.h
src/build.c
src/main.c
src/pager.c
src/pager.h
src/parse.y
src/sqliteInt.h
src/vacuum.c
src/vdbe.c
src/vdbeaux.c
src/wal.c
src/wal.h
test/unlocked.test [new file with mode: 0644]
tool/mkkeywordhash.c

index 9db7a695879b1fefa91b5c59bf93e1102285b265..62addb80e642db9d31bb525a85d4ba14b8cd0801 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Version\s3.8.11
-D 2015-07-27T13:49:41.754
+C Add\san\sexperimental\s"BEGIN\sUNLOCKED"\scommand.
+D 2015-07-27T19:31:45.102
 F Makefile.arm-wince-mingw32ce-gcc d6df77f1f48d690bd73162294bbba7f59507c72f
 F Makefile.in 4de3ef40c8b3b75c0c55ff4242a43c8ce1ad90ee
 F Makefile.linux-gcc 91d710bdc4998cb015f39edf3cb314ec4f4d7e23
@@ -270,10 +270,10 @@ F src/auth.c b56c78ebe40a2110fd361379f7e8162d23f92240
 F src/backup.c 4d9134dc988a87838c06056c89c0e8c4700a0452
 F src/bitvec.c d1f21d7d91690747881f03940584f4cc548c9d3d
 F src/btmutex.c 45a968cc85afed9b5e6cf55bf1f42f8d18107f79
-F src/btree.c f48b3ef91676c06a90a8832987ecef6b94c931ee
-F src/btree.h 969adc948e89e449220ff0ff724c94bb2a52e9f1
+F src/btree.c b184ce6d483dfadc53126a4b7994633ec0d5ef5f
+F src/btree.h 550e8447684013a69196ec664142f67538da8ef1
 F src/btreeInt.h 2ad754dd4528baa8d0946a593cc373b890bf859e
-F src/build.c b3f15255d5b16e42dafeaa638fd4f8a47c94ed70
+F src/build.c 28c15c43eefc0066ff64040526ff649c32fe5523
 F src/callback.c 7b44ce59674338ad48b0e84e7b72f935ea4f68b0
 F src/complete.c addcd8160b081131005d5bc2d34adf20c1c5c92f
 F src/ctime.c 5a0b735dc95604766f5dac73973658eef782ee8b
@@ -293,7 +293,7 @@ F src/journal.c b4124532212b6952f42eb2c12fa3c25701d8ba8d
 F src/legacy.c ba1863ea58c4c840335a84ec276fc2b25e22bc4e
 F src/lempar.c 92bafa308607dd985ca389a788cd9e0a2b608712
 F src/loadext.c dfcee8c7c032cd0fd55af3e0fc1fcfb01e426df2
-F src/main.c 0a60b7ca8252c3a6f95438fa4ce8fe5b275c69f2
+F src/main.c ebbd5a45caf66ce0ebdf1b6c0bc4784e3cfcd768
 F src/malloc.c 19461e159bccf0e2cf06a50e867963d0a7b124a8
 F src/mem0.c 6a55ebe57c46ca1a7d98da93aaa07f99f1059645
 F src/mem1.c abe6ee469b6c5a35c7f22bfeb9c9bac664a1c987
@@ -315,9 +315,9 @@ F src/os_setup.h c9d4553b5aaa6f73391448b265b89bed0b890faa
 F src/os_unix.c 388c023582b17890f10c980b30ec1922b471753b
 F src/os_win.c 40b3af7a47eb1107d0d69e592bec345a3b7b798a
 F src/os_win.h eb7a47aa17b26b77eb97e4823f20a00b8bda12ca
-F src/pager.c aa916ca28606ccf4b6877dfc2b643ccbca86589f
-F src/pager.h 6d435f563b3f7fcae4b84433b76a6ac2730036e2
-F src/parse.y 6d60dda8f8d418b6dc034f1fbccd816c459983a8
+F src/pager.c c9f22eb831851220c1095863c5797a88f71bea8a
+F src/pager.h b6cf6e9552c5b1e7d6d0d8cd7ca51be7802bec72
+F src/parse.y 199145cd982587d70fa3db2f9b11f7ab118f0acd
 F src/pcache.c cde06aa50962595e412d497e22fd2e07878ba1f0
 F src/pcache.h 9968603796240cdf83da7e7bef76edf90619cea9
 F src/pcache1.c d08939800abf3031bd0affd5a13fbc4d7ba3fb68
@@ -333,7 +333,7 @@ F src/shell.c 8af3cced094aebb5f57a8ad739b9dafc7867eed7
 F src/sqlite.h.in f623dd30a4fb7df2fb44a2a85e27813d25e486c2
 F src/sqlite3.rc 992c9f5fb8285ae285d6be28240a7e8d3a7f2bad
 F src/sqlite3ext.h a0b948ebc89bac13941254641326a6aa248c2cc4
-F src/sqliteInt.h 81f458941cfb30c5536c37930fb6f41e66171284
+F src/sqliteInt.h f14033fa6da3ca70f7dde5ba19dc1ed58055a2d7
 F src/sqliteLimit.h 216557999cb45f2e3578ed53ebefe228d779cb46
 F src/status.c f266ad8a2892d659b74f0f50cb6a88b6e7c12179
 F src/table.c 51b46b2a62d1b3a959633d593b89bab5e2c9155e
@@ -390,20 +390,20 @@ F src/trigger.c 322f23aad694e8f31d384dcfa386d52a48d3c52f
 F src/update.c 487747b328b7216bb7f6af0695d6937d5c9e605f
 F src/utf.c fc6b889ba0779b7722634cdeaa25f1930d93820c
 F src/util.c 46358a204b35971a839341cf64599d65b151ba88
-F src/vacuum.c 2ddd5cad2a7b9cef7f9e431b8c7771634c6b1701
-F src/vdbe.c 86be40a4568f5385048ea6623adfd11ddb7bf710
+F src/vacuum.c 4e7e18d889662eda281855a7125e56269069f215
+F src/vdbe.c fd6832408d9621ad347a65b01a05380fe766232f
 F src/vdbe.h 7a75045d879118b9d3af7e8b3c108f2f27c51473
 F src/vdbeInt.h 8b54e01ad0463590e7cffabce0bc36da9ee4f816
 F src/vdbeapi.c adabbd66eb2e3a10f3998485ee0be7e326d06ee4
-F src/vdbeaux.c 787f5f9d58f4c6f39294ed06909ba602d1a402e6
+F src/vdbeaux.c 27feae106f3f4db298e65d37a9da4962aaff724b
 F src/vdbeblob.c 4f2e8e075d238392df98c5e03a64342465b03f90
 F src/vdbemem.c ae38a0d35ae71cf604381a887c170466ba518090
 F src/vdbesort.c f5009e7a35e3065635d8918b9a31f498a499976b
 F src/vdbetrace.c 8befe829faff6d9e6f6e4dee5a7d3f85cc85f1a0
 F src/vtab.c 082b35a25a26e3d36f365ca8cd73c1922532f05e
 F src/vxworks.h c18586c8edc1bddbc15c004fa16aeb1e1342b4fb
-F src/wal.c 6fb6b68969e4692593c2552c4e7bff5882de2cb8
-F src/wal.h df01efe09c5cb8c8e391ff1715cca294f89668a4
+F src/wal.c d588e26ed0d912584551dfe65d644d8c27cc2d1f
+F src/wal.h 5188cd85398dc612ae5ed2ed02d193377218be56
 F src/walker.c c253b95b4ee44b21c406e2a1052636c31ea27804
 F src/where.c 909eba3b6db984eb2adfbca9de2c237ee7056adb
 F src/whereInt.h 5f87e3c4b0551747d119730dfebddd3c54f04047
@@ -1215,6 +1215,7 @@ F test/types3.test 99e009491a54f4dc02c06bdbc0c5eea56ae3e25a
 F test/unique.test 93f8b2ef5ea51b9495f8d6493429b1fd0f465264
 F test/unique2.test 41e7f83c6827605991160a31380148a9fc5f1339
 F test/unixexcl.test cd6c765f75e50e8e2c2ba763149e5d340ea19825
+F test/unlocked.test edd4ed073ab16c0f8c9f5124e039100657b422c9
 F test/unordered.test ca7adce0419e4ca0c50f039885e76ed2c531eda8
 F test/update.test 6c68446b8a0a33d522a7c72b320934596a2d7d32
 F test/uri.test 23662b7b61958b0f0e47082de7d06341ccf85d5b
@@ -1326,7 +1327,7 @@ F tool/lempar.c 01ca97f87610d1dac6d8cd96ab109ab1130e76dc
 F tool/loadfts.c c3c64e4d5e90e8ba41159232c2189dba4be7b862
 F tool/logest.c eef612f8adf4d0993dafed0416064cf50d5d33c6
 F tool/mkautoconfamal.sh d1a2da0e15b2ed33d60af35c7e9d483f13a8eb9f
-F tool/mkkeywordhash.c dfff09dbbfaf950e89af294f48f902181b144670
+F tool/mkkeywordhash.c de4a823fe66f9e8c39c86e465ac7285fd6935bb8
 F tool/mkopts.tcl 66ac10d240cc6e86abd37dc908d50382f84ff46e
 F tool/mkpragmatab.tcl 84af2b180484323a2ea22a2279e8bd9e3e1e492e
 F tool/mkspeedsql.tcl a1a334d288f7adfe6e996f2e712becf076745c97
@@ -1366,10 +1367,10 @@ F tool/vdbe_profile.tcl 67746953071a9f8f2f668b73fe899074e2c6d8c1
 F tool/warnings-clang.sh f6aa929dc20ef1f856af04a730772f59283631d4
 F tool/warnings.sh 48bd54594752d5be3337f12c72f28d2080cb630b
 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f
-P 9f1dcdc3e1644c21762dacf619fac70afe6318c5
-R 8f58f88db8a7564cc507b9707cc8c853
-T +bgcolor * #d0c0ff
-T +sym-release *
-T +sym-version-3.8.11 *
-U drh
-Z ff777afb71751a2633799d5aca45576d
+P b8e92227a469de677a66da62e4361f099c0b79d0
+R 889966cd4fab23d4f4355c880be4edd8
+T *branch * experimental-begin-unlocked
+T *sym-experimental-begin-unlocked *
+T -sym-trunk *
+U dan
+Z 55f5e7a6a42bdc95bbb980b319cd8658
index 8247151f16582f8458b20dee7271a50351314baf..25c90061984d72815d9ad8c2409d3378f1050c9b 100644 (file)
@@ -1 +1 @@
-b8e92227a469de677a66da62e4361f099c0b79d0
\ No newline at end of file
+8079421604dbd40d03471dad6d12115119b554c2
\ No newline at end of file
index 2692ade8e4888fc98904b8d047603ad95a650b51..c83a01ad4cb2bf41f4fb0e18fc0d5ed4e9bfd4a7 100644 (file)
@@ -3142,7 +3142,10 @@ int sqlite3BtreeBeginTrans(Btree *p, int wrflag){
       if( (pBt->btsFlags & BTS_READ_ONLY)!=0 ){
         rc = SQLITE_READONLY;
       }else{
-        rc = sqlite3PagerBegin(pBt->pPager,wrflag>1,sqlite3TempInMemory(p->db));
+        int bSubjInMem = sqlite3TempInMemory(p->db);
+        int exFlag = p->db->bUnlocked ? -1 : (wrflag>1);
+        assert( p->db->bUnlocked==0 || wrflag==1 );
+        rc = sqlite3PagerBegin(pBt->pPager, exFlag, bSubjInMem);
         if( rc==SQLITE_OK ){
           rc = newDatabase(pBt);
         }
@@ -3670,8 +3673,15 @@ int sqlite3BtreeCommitPhaseOne(Btree *p, const char *zMaster){
   if( p->inTrans==TRANS_WRITE ){
     BtShared *pBt = p->pBt;
     sqlite3BtreeEnter(p);
+
 #ifndef SQLITE_OMIT_AUTOVACUUM
-    if( pBt->autoVacuum ){
+    /* Figure out if this is a commit of an UNLOCKED transaction that 
+    ** requires a snapshot upgrade. If so, skip any auto-vacuum 
+    ** processing.  */
+    if( pBt->autoVacuum && (
+        0==pBt->db->bUnlocked
+     || 0==sqlite3PagerCommitRequiresUpgrade(pBt->pPager) 
+    )){
       rc = autoVacuumCommit(pBt);
       if( rc!=SQLITE_OK ){
         sqlite3BtreeLeave(p);
@@ -3682,7 +3692,9 @@ int sqlite3BtreeCommitPhaseOne(Btree *p, const char *zMaster){
       sqlite3PagerTruncateImage(pBt->pPager, pBt->nPage);
     }
 #endif
-    rc = sqlite3PagerCommitPhaseOne(pBt->pPager, zMaster, 0);
+    if( rc==SQLITE_OK ){
+      rc = sqlite3PagerCommitPhaseOne(pBt->pPager, zMaster, 0);
+    }
     sqlite3BtreeLeave(p);
   }
   return rc;
@@ -9578,3 +9590,15 @@ int sqlite3BtreeIsReadonly(Btree *p){
 ** Return the size of the header added to each page by this module.
 */
 int sqlite3HeaderSizeBtree(void){ return ROUND8(sizeof(MemPage)); }
+
+int sqlite3BtreeExclusiveLock(Btree *p){
+  int rc;
+  BtShared *pBt = p->pBt;
+  sqlite3BtreeEnter(p);
+  rc = sqlite3PagerExclusiveLock(pBt->pPager, pBt->pPage1->pDbPage);
+  sqlite3BtreeLeave(p);
+  return rc;
+}
+
+
+
index 3edc2b3b57739dca09b7c0ee9ed9e1847ba2d2ad..ae9b2c4e59951000be0d0e0cd42b1205a92b9863 100644 (file)
@@ -270,5 +270,6 @@ void sqlite3BtreeCursorList(Btree*);
 # define sqlite3SchemaMutexHeld(X,Y,Z) 1
 #endif
 
+int sqlite3BtreeExclusiveLock(Btree*);
 
 #endif /* _BTREE_H_ */
index 2936805365fe558941f362a1f894f0923579262d..0300e2fc15039aa7abf81ba74374a471697d3f1b 100644 (file)
@@ -3828,13 +3828,13 @@ void sqlite3BeginTransaction(Parse *pParse, int type){
   }
   v = sqlite3GetVdbe(pParse);
   if( !v ) return;
-  if( type!=TK_DEFERRED ){
+  if( type==TK_IMMEDIATE || type==TK_EXCLUSIVE ){
     for(i=0; i<db->nDb; i++){
       sqlite3VdbeAddOp2(v, OP_Transaction, i, (type==TK_EXCLUSIVE)+1);
       sqlite3VdbeUsesBtree(v, i);
     }
   }
-  sqlite3VdbeAddOp2(v, OP_AutoCommit, 0, 0);
+  sqlite3VdbeAddOp3(v, OP_AutoCommit, 0, 0, (type==TK_UNLOCKED));
 }
 
 /*
index 36206eec8cacd92187fe7b07253b69f553c610bc..27bb2f0e8378156d7789e4064de1e6be972521fd 100644 (file)
@@ -2737,6 +2737,7 @@ static int openDatabase(
   memcpy(db->aLimit, aHardLimit, sizeof(db->aLimit));
   db->aLimit[SQLITE_LIMIT_WORKER_THREADS] = SQLITE_DEFAULT_WORKER_THREADS;
   db->autoCommit = 1;
+  db->bUnlocked = 0;
   db->nextAutovac = -1;
   db->szMmap = sqlite3GlobalConfig.szMmap;
   db->nextPagesize = 0;
index 060edb8d1dd7755b30cacdc5a090dbc03fcb66c7..2ea3b90a96b0f9c84b50e50384cf69302cedd1d9 100644 (file)
@@ -3066,6 +3066,7 @@ static int pagerWalFrames(
     ** list here. */
     PgHdr **ppNext = &pList;
     nList = 0;
+
     for(p=pList; (*ppNext = p)!=0; p=p->pDirty){
       if( p->pgno<=nTruncate ){
         ppNext = &p->pDirty;
@@ -4081,7 +4082,7 @@ static int syncJournal(Pager *pPager, int newHdr){
   assert( assert_pager_state(pPager) );
   assert( !pagerUseWal(pPager) );
 
-  rc = sqlite3PagerExclusiveLock(pPager);
+  rc = sqlite3PagerExclusiveLock(pPager, 0);
   if( rc!=SQLITE_OK ) return rc;
 
   if( !pPager->noSync ){
@@ -4429,6 +4430,10 @@ static int pagerStress(void *p, PgHdr *pPg){
 
   pPg->pDirty = 0;
   if( pagerUseWal(pPager) ){
+    /* If the transaction is a "BEGIN UNLOCKED" transaction, the page 
+    ** cannot be flushed to disk. Return early in this case. */
+    if( sqlite3WalIsInTrans(pPager->pWal)==0 ) return SQLITE_OK;
+
     /* Write a single frame for this page to the log. */
     rc = subjournalPageIfRequired(pPg); 
     if( rc==SQLITE_OK ){
@@ -5548,11 +5553,14 @@ static int pager_open_journal(Pager *pPager){
 ** Begin a write-transaction on the specified pager object. If a 
 ** write-transaction has already been opened, this function is a no-op.
 **
-** If the exFlag argument is false, then acquire at least a RESERVED
-** lock on the database file. If exFlag is true, then acquire at least
+** If the exFlag argument is 0, then acquire at least a RESERVED
+** lock on the database file. If exFlag is >0, then acquire at least
 ** an EXCLUSIVE lock. If such a lock is already held, no locking 
 ** functions need be called.
 **
+** If (exFlag<0) and the database is in WAL mode, do not take any locks.
+** The transaction will run in UNLOCKED mode instead.
+**
 ** If the subjInMemory argument is non-zero, then any sub-journal opened
 ** within this transaction will be opened as an in-memory file. This
 ** has no effect if the sub-journal is already opened (as it may be when
@@ -5588,7 +5596,9 @@ int sqlite3PagerBegin(Pager *pPager, int exFlag, int subjInMemory){
       ** The busy-handler is not invoked if another connection already
       ** holds the write-lock. If possible, the upper layer will call it.
       */
-      rc = sqlite3WalBeginWriteTransaction(pPager->pWal);
+      if( exFlag>=0 ){
+        rc = sqlite3WalBeginWriteTransaction(pPager->pWal);
+      }
     }else{
       /* Obtain a RESERVED lock on the database file. If the exFlag parameter
       ** is true, then immediately upgrade this to an EXCLUSIVE lock. The
@@ -5596,7 +5606,7 @@ int sqlite3PagerBegin(Pager *pPager, int exFlag, int subjInMemory){
       ** lock, but not when obtaining the RESERVED lock.
       */
       rc = pagerLockDb(pPager, RESERVED_LOCK);
-      if( rc==SQLITE_OK && exFlag ){
+      if( rc==SQLITE_OK && exFlag>0 ){
         rc = pager_wait_on_lock(pPager, EXCLUSIVE_LOCK);
       }
     }
@@ -6056,7 +6066,7 @@ int sqlite3PagerSync(Pager *pPager, const char *zMaster){
 ** Otherwise, either SQLITE_BUSY or an SQLITE_IOERR_XXX error code is 
 ** returned.
 */
-int sqlite3PagerExclusiveLock(Pager *pPager){
+int sqlite3PagerExclusiveLock(Pager *pPager, PgHdr *pPage1){
   int rc = SQLITE_OK;
   assert( pPager->eState==PAGER_WRITER_CACHEMOD 
        || pPager->eState==PAGER_WRITER_DBMOD 
@@ -6065,10 +6075,52 @@ int sqlite3PagerExclusiveLock(Pager *pPager){
   assert( assert_pager_state(pPager) );
   if( 0==pagerUseWal(pPager) ){
     rc = pager_wait_on_lock(pPager, EXCLUSIVE_LOCK);
+  }else{
+    Wal *pWal = pPager->pWal;
+    if( 0==sqlite3WalIsInTrans(pWal) ){
+      /* TODO: There must be an optimization opportunity here, as this call 
+      ** to PcacheDirtyList() sorts the list of dirty pages, even though it 
+      ** is not really required - and will be sorted again in CommitPhaseOne()
+      ** in any case.  */
+      PgHdr *pList = sqlite3PcacheDirtyList(pPager->pPCache);
+
+      /* This is an UNLOCKED transaction. Attempt to lock the wal database
+      ** here. If SQLITE_BUSY (but not SQLITE_BUSY_SNAPSHOT) is returned,
+      ** invoke the busy-handler and try again for as long as it returns
+      ** non-zero.  */
+      do {
+        /* rc = sqlite3WalBeginWriteTransaction(pWal); */
+        rc = sqlite3WalLockForCommit(pWal, pList, pPage1); 
+      }while( rc==SQLITE_BUSY 
+           && pPager->xBusyHandler(pPager->pBusyHandlerArg) 
+      );
+    }
   }
   return rc;
 }
 
+/*
+** If this is a WAL mode connection and the WRITER lock is currently held,
+** relinquish it.
+*/
+void sqlite3PagerDropExclusiveLock(Pager *pPager){
+  if( pagerUseWal(pPager) ){
+    sqlite3WalEndWriteTransaction(pPager->pWal);
+  }
+}
+
+/*
+** Return true if this is a WAL database and snapshot upgrade is required
+** before the current transaction can be committed.
+*/
+int sqlite3PagerCommitRequiresUpgrade(Pager *pPager){
+  int res = 0;
+  if( pagerUseWal(pPager) ){
+    res = sqlite3WalCommitRequiresUpgrade(pPager->pWal);
+  }
+  return res;
+}
+
 /*
 ** Sync the database file for the pager pPager. zMaster points to the name
 ** of a master journal file that should be written into the individual
index e3b57f435e1ea0af92ca6156487ba5b4c47f4b32..57d2812701044ab0692f0286cef1de592534dafa 100644 (file)
@@ -150,7 +150,7 @@ void *sqlite3PagerGetExtra(DbPage *);
 void sqlite3PagerPagecount(Pager*, int*);
 int sqlite3PagerBegin(Pager*, int exFlag, int);
 int sqlite3PagerCommitPhaseOne(Pager*,const char *zMaster, int);
-int sqlite3PagerExclusiveLock(Pager*);
+int sqlite3PagerExclusiveLock(Pager*, DbPage *pPage1);
 int sqlite3PagerSync(Pager *pPager, const char *zMaster);
 int sqlite3PagerCommitPhaseTwo(Pager*);
 int sqlite3PagerRollback(Pager*);
@@ -158,6 +158,9 @@ int sqlite3PagerOpenSavepoint(Pager *pPager, int n);
 int sqlite3PagerSavepoint(Pager *pPager, int op, int iSavepoint);
 int sqlite3PagerSharedLock(Pager *pPager);
 
+void sqlite3PagerDropExclusiveLock(Pager*);
+int sqlite3PagerCommitRequiresUpgrade(Pager*);
+
 #ifndef SQLITE_OMIT_WAL
   int sqlite3PagerCheckpoint(Pager *pPager, int, int*, int*);
   int sqlite3PagerWalSupported(Pager *pPager);
index d7aa763683348caaa943b078da04ed5aa0600d8e..db40b70b9cd081e631ae5e7b5c3de89c0e097149 100644 (file)
@@ -121,6 +121,7 @@ transtype(A) ::= .             {A = TK_DEFERRED;}
 transtype(A) ::= DEFERRED(X).  {A = @X;}
 transtype(A) ::= IMMEDIATE(X). {A = @X;}
 transtype(A) ::= EXCLUSIVE(X). {A = @X;}
+transtype(A) ::= UNLOCKED(X).  {A = @X;}
 cmd ::= COMMIT trans_opt.      {sqlite3CommitTransaction(pParse);}
 cmd ::= END trans_opt.         {sqlite3CommitTransaction(pParse);}
 cmd ::= ROLLBACK trans_opt.    {sqlite3RollbackTransaction(pParse);}
index d26cd19eb7201eeb2db6e9c6e2fbea45fd7d7f68..2480bb0cfe5b3f98cd87b9f643fd26de6efc828f 100644 (file)
@@ -1147,6 +1147,7 @@ struct sqlite3 {
   u16 dbOptFlags;               /* Flags to enable/disable optimizations */
   u8 enc;                       /* Text encoding */
   u8 autoCommit;                /* The auto-commit flag. */
+  u8 bUnlocked;                 /* Current transaction is "UNLOCKED" */
   u8 temp_store;                /* 1: file 2: memory 0: default */
   u8 mallocFailed;              /* True if we have seen a malloc failure */
   u8 dfltLockMode;              /* Default locking-mode for attached dbs */
index adc802e60b4330608c511829857dd14c74c597ce..10f14cbf47940823fb1005209056ffb284abf940 100644 (file)
@@ -356,6 +356,7 @@ end_of_vacuum:
   ** is closed by the DETACH.
   */
   db->autoCommit = 1;
+  db->bUnlocked = 0;
 
   if( pDb ){
     sqlite3BtreeClose(pDb->pBt);
index fe97087c0986d9bee2d983044344a13d88a8716d..b400c425b88a6630cce4d1b6c36aa05df6ffb80d 100644 (file)
@@ -2899,6 +2899,7 @@ case OP_Savepoint: {
         if( (rc = sqlite3VdbeCheckFk(p, 1))!=SQLITE_OK ){
           goto vdbe_return;
         }
+        assert( db->bUnlocked==0 );
         db->autoCommit = 1;
         if( sqlite3VdbeHalt(p)==SQLITE_BUSY ){
           p->pc = (int)(pOp - aOp);
@@ -2970,29 +2971,39 @@ case OP_Savepoint: {
   break;
 }
 
-/* Opcode: AutoCommit P1 P2 * * *
+/* Opcode: AutoCommit P1 P2 P3 * *
 **
 ** Set the database auto-commit flag to P1 (1 or 0). If P2 is true, roll
 ** back any currently active btree transactions. If there are any active
 ** VMs (apart from this one), then a ROLLBACK fails.  A COMMIT fails if
 ** there are active writing VMs or active VMs that use shared cache.
 **
+** If P3 is non-zero, then this instruction is being executed as part of
+** a "BEGIN UNLOCKED" command.
+**
 ** This instruction causes the VM to halt.
 */
 case OP_AutoCommit: {
   int desiredAutoCommit;
   int iRollback;
   int turnOnAC;
+  int bUnlocked;
+  int hrc;
 
   desiredAutoCommit = pOp->p1;
   iRollback = pOp->p2;
+  bUnlocked = pOp->p3;
   turnOnAC = desiredAutoCommit && !db->autoCommit;
   assert( desiredAutoCommit==1 || desiredAutoCommit==0 );
   assert( desiredAutoCommit==1 || iRollback==0 );
+  assert( desiredAutoCommit==0 || bUnlocked==0 );
+  assert( db->autoCommit==0 || db->bUnlocked==0 );
   assert( db->nVdbeActive>0 );  /* At least this one VM is active */
   assert( p->bIsReader );
 
-  if( turnOnAC && !iRollback && db->nVdbeWrite>0 ){
+  if( turnOnAC && !iRollback && 
+      (db->nVdbeWrite>0 || (db->bUnlocked && db->nVdbeActive>1))
+  ){
     /* If this instruction implements a COMMIT and other VMs are writing
     ** return an error indicating that the other VMs must complete first. 
     */
@@ -3004,16 +3015,20 @@ case OP_AutoCommit: {
       assert( desiredAutoCommit==1 );
       sqlite3RollbackAll(db, SQLITE_ABORT_ROLLBACK);
       db->autoCommit = 1;
+      db->bUnlocked = 0;
     }else if( (rc = sqlite3VdbeCheckFk(p, 1))!=SQLITE_OK ){
       goto vdbe_return;
     }else{
       db->autoCommit = (u8)desiredAutoCommit;
-      if( sqlite3VdbeHalt(p)==SQLITE_BUSY ){
+      hrc = sqlite3VdbeHalt(p);
+      if( (hrc & 0xFF)==SQLITE_BUSY ){
         p->pc = (int)(pOp - aOp);
         db->autoCommit = (u8)(1-desiredAutoCommit);
-        p->rc = rc = SQLITE_BUSY;
+        p->rc = hrc;
+        rc = SQLITE_BUSY;
         goto vdbe_return;
       }
+      db->bUnlocked = (u8)bUnlocked;
     }
     assert( db->nStatement==0 );
     sqlite3CloseSavepoints(db);
@@ -3208,9 +3223,16 @@ case OP_SetCookie: {       /* in3 */
   /* See note about index shifting on OP_ReadCookie */
   rc = sqlite3BtreeUpdateMeta(pDb->pBt, pOp->p2, (int)pIn3->u.i);
   if( pOp->p2==BTREE_SCHEMA_VERSION ){
-    /* When the schema cookie changes, record the new cookie internally */
-    pDb->pSchema->schema_cookie = (int)pIn3->u.i;
-    db->flags |= SQLITE_InternChanges;
+    if( db->bUnlocked ){
+      sqlite3VdbeError(p, "cannot modify database schema - "
+          "UNLOCKED transaction"
+      );
+      rc = SQLITE_ERROR;
+    }else{
+      /* When the schema cookie changes, record the new cookie internally */
+      pDb->pSchema->schema_cookie = (int)pIn3->u.i;
+      db->flags |= SQLITE_InternChanges;
+    }
   }else if( pOp->p2==BTREE_FILE_FORMAT ){
     /* Record changes in the file format */
     pDb->pSchema->file_format = (u8)pIn3->u.i;
index 08dc885df67528c601050ba1ad891e260cb472ed..f5f9d0fbdda4feda9a6ba963f0befa9396feb456 100644 (file)
@@ -2020,11 +2020,24 @@ static int vdbeCommit(sqlite3 *db, Vdbe *p){
     if( sqlite3BtreeIsInTrans(pBt) ){
       needXcommit = 1;
       if( i!=1 ) nTrans++;
-      sqlite3BtreeEnter(pBt);
-      rc = sqlite3PagerExclusiveLock(sqlite3BtreePager(pBt));
-      sqlite3BtreeLeave(pBt);
+      rc = sqlite3BtreeExclusiveLock(pBt);
     }
   }
+
+  if( db->bUnlocked && (rc & 0xFF)==SQLITE_BUSY ){
+    /* An SQLITE_BUSY or SQLITE_BUSY_SNAPSHOT was encountered while 
+    ** attempting to take the WRITER lock on a wal file. Release the
+    ** WRITER locks on all wal files and return early.  */
+    for(i=0; i<db->nDb; i++){
+      Btree *pBt = db->aDb[i].pBt;
+      if( sqlite3BtreeIsInTrans(pBt) ){
+        sqlite3BtreeEnter(pBt);
+        sqlite3PagerDropExclusiveLock(sqlite3BtreePager(pBt));
+        sqlite3BtreeLeave(pBt);
+      }
+    }
+  }
+
   if( rc!=SQLITE_OK ){
     return rc;
   }
@@ -2427,6 +2440,7 @@ int sqlite3VdbeHalt(Vdbe *p){
           sqlite3RollbackAll(db, SQLITE_ABORT_ROLLBACK);
           sqlite3CloseSavepoints(db);
           db->autoCommit = 1;
+          db->bUnlocked = 0;
           p->nChange = 0;
         }
       }
@@ -2462,9 +2476,9 @@ int sqlite3VdbeHalt(Vdbe *p){
           ** is required. */
           rc = vdbeCommit(db, p);
         }
-        if( rc==SQLITE_BUSY && p->readOnly ){
+        if( (rc & 0xFF)==SQLITE_BUSY && p->readOnly ){
           sqlite3VdbeLeave(p);
-          return SQLITE_BUSY;
+          return rc;
         }else if( rc!=SQLITE_OK ){
           p->rc = rc;
           sqlite3RollbackAll(db, SQLITE_OK);
@@ -2489,6 +2503,7 @@ int sqlite3VdbeHalt(Vdbe *p){
         sqlite3RollbackAll(db, SQLITE_ABORT_ROLLBACK);
         sqlite3CloseSavepoints(db);
         db->autoCommit = 1;
+        db->bUnlocked = 0;
         p->nChange = 0;
       }
     }
@@ -2510,6 +2525,7 @@ int sqlite3VdbeHalt(Vdbe *p){
         sqlite3RollbackAll(db, SQLITE_ABORT_ROLLBACK);
         sqlite3CloseSavepoints(db);
         db->autoCommit = 1;
+        db->bUnlocked = 0;
         p->nChange = 0;
       }
     }
@@ -2554,7 +2570,7 @@ int sqlite3VdbeHalt(Vdbe *p){
   }
 
   assert( db->nVdbeActive>0 || db->autoCommit==0 || db->nStatement==0 );
-  return (p->rc==SQLITE_BUSY ? SQLITE_BUSY : SQLITE_OK);
+  return ((p->rc & 0xFF)==SQLITE_BUSY ? SQLITE_BUSY : SQLITE_OK);
 }
 
 
index f7e2594001c257a863347eda396ec849d859096a..a2200e70059fcd198b27063795688b550055792d 100644 (file)
--- a/src/wal.c
+++ b/src/wal.c
@@ -2357,38 +2357,19 @@ void sqlite3WalEndReadTransaction(Wal *pWal){
 }
 
 /*
-** Search the wal file for page pgno. If found, set *piRead to the frame that
-** contains the page. Otherwise, if pgno is not in the wal file, set *piRead
-** to zero.
-**
-** Return SQLITE_OK if successful, or an error code if an error occurs. If an
-** error does occur, the final value of *piRead is undefined.
-*/
-int sqlite3WalFindFrame(
-  Wal *pWal,                      /* WAL handle */
-  Pgno pgno,                      /* Database page number to read data for */
-  u32 *piRead                     /* OUT: Frame number (or zero) */
+** Search the hash tables for an entry matching page number pgno. Ignore
+** any entries that lie after frame iLast within the wal file.
+*/
+static int walFindFrame(
+  Wal *pWal, 
+  Pgno pgno, 
+  u32 iLast, 
+  u32 *piRead
 ){
-  u32 iRead = 0;                  /* If !=0, WAL frame to return data from */
-  u32 iLast = pWal->hdr.mxFrame;  /* Last page in WAL for this reader */
   int iHash;                      /* Used to loop through N hash tables */
+  u32 iRead = 0;
 
-  /* This routine is only be called from within a read transaction. */
-  assert( pWal->readLock>=0 || pWal->lockError );
-
-  /* If the "last page" field of the wal-index header snapshot is 0, then
-  ** no data will be read from the wal under any circumstances. Return early
-  ** in this case as an optimization.  Likewise, if pWal->readLock==0, 
-  ** then the WAL is ignored by the reader so return early, as if the 
-  ** WAL were empty.
-  */
-  if( iLast==0 || pWal->readLock==0 ){
-    *piRead = 0;
-    return SQLITE_OK;
-  }
-
-  /* Search the hash table or tables for an entry matching page number
-  ** pgno. Each iteration of the following for() loop searches one
+  /* Each iteration of the following for() loop searches one
   ** hash table (each hash table indexes up to HASHTABLE_NPAGE frames).
   **
   ** This code might run concurrently to the code in walIndexAppend()
@@ -2437,11 +2418,48 @@ int sqlite3WalFindFrame(
     }
   }
 
+  *piRead = iRead;
+  return SQLITE_OK;
+}
+
+/*
+** Search the wal file for page pgno. If found, set *piRead to the frame that
+** contains the page. Otherwise, if pgno is not in the wal file, set *piRead
+** to zero.
+**
+** Return SQLITE_OK if successful, or an error code if an error occurs. If an
+** error does occur, the final value of *piRead is undefined.
+*/
+int sqlite3WalFindFrame(
+  Wal *pWal,                      /* WAL handle */
+  Pgno pgno,                      /* Database page number to read data for */
+  u32 *piRead                     /* OUT: Frame number (or zero) */
+){
+  u32 iRead = 0;                  /* If !=0, WAL frame to return data from */
+  u32 iLast = pWal->hdr.mxFrame;  /* Last page in WAL for this reader */
+  int rc;
+
+  /* This routine is only be called from within a read transaction. */
+  assert( pWal->readLock>=0 || pWal->lockError );
+
+  /* If the "last page" field of the wal-index header snapshot is 0, then
+  ** no data will be read from the wal under any circumstances. Return early
+  ** in this case as an optimization.  Likewise, if pWal->readLock==0, 
+  ** then the WAL is ignored by the reader so return early, as if the 
+  ** WAL were empty.
+  */
+  if( iLast==0 || pWal->readLock==0 ){
+    *piRead = 0;
+    return SQLITE_OK;
+  }
+
+  rc = walFindFrame(pWal, pgno, iLast, &iRead);
+
 #ifdef SQLITE_ENABLE_EXPENSIVE_ASSERT
   /* If expensive assert() statements are available, do a linear search
   ** of the wal-index file content. Make sure the results agree with the
   ** result obtained using the hash indexes above.  */
-  {
+  if( rc==SQLITE_OK ){
     u32 iRead2 = 0;
     u32 iTest;
     for(iTest=iLast; iTest>0; iTest--){
@@ -2537,6 +2555,96 @@ int sqlite3WalBeginWriteTransaction(Wal *pWal){
   return rc;
 }
 
+/* 
+** TODO: Combine some code with BeginWriteTransaction()
+**
+** This function is only ever called when committing a "BEGIN UNLOCKED"
+** transaction. It may be assumed that no frames have been written to
+** the wal file.
+*/
+int sqlite3WalLockForCommit(Wal *pWal, PgHdr *pList, PgHdr *pPage1){
+  volatile WalIndexHdr *pHead;    /* Head of the wal file */
+  int rc;
+
+  /* Cannot start a write transaction without first holding a read
+  ** transaction. */
+  assert( pWal->readLock>=0 );
+
+  if( pWal->readOnly ){
+    return SQLITE_READONLY;
+  }
+
+  /* Only one writer allowed at a time.  Get the write lock.  Return
+  ** SQLITE_BUSY if unable.
+  */
+  rc = walLockExclusive(pWal, WAL_WRITE_LOCK, 1, 0);
+  if( rc ){
+    return rc;
+  }
+  pWal->writeLock = 1;
+
+  /* If the database has been modified since this transaction was started,
+  ** check if it is still possible to commit. The transaction can be 
+  ** committed if:
+  **
+  **   a) None of the pages in pList have been modified since the 
+  **      transaction opened, and
+  **
+  **   b) The database schema cookie has not been modified since the
+  **      transaction was started.
+  */
+  pHead = walIndexHdr(pWal);
+  if( memcmp(&pWal->hdr, (void*)pHead, sizeof(WalIndexHdr))!=0 ){
+    /* TODO: Is this safe? Because it holds the WRITER lock this thread
+    ** has exclusive access to the live header, but might it be corrupt? */
+    PgHdr *pPg;
+    u32 iLast = pHead->mxFrame;
+    for(pPg=pList; rc==SQLITE_OK && pPg; pPg=pPg->pDirty){
+      u32 iSlot = 0;
+      rc = walFindFrame(pWal, pPg->pgno, iLast, &iSlot);
+      if( iSlot>pWal->hdr.mxFrame ){
+        sqlite3_log(SQLITE_OK,
+            "cannot commit UNLOCKED transaction (conflict at page %d)",
+            (int)pPg->pgno
+        );
+        rc = SQLITE_BUSY_SNAPSHOT;
+      }
+    }
+
+    if( rc==SQLITE_OK ){
+      /* Read the newest schema cookie from the wal file. */
+      u32 iSlot = 0;
+      rc = walFindFrame(pWal, 1, iLast, &iSlot);
+      if( rc==SQLITE_OK && iSlot>pWal->hdr.mxFrame ){
+        u8 aNew[4];
+        u8 *aOld = &((u8*)pPage1->pData)[40];
+        int sz;
+        i64 iOffset;
+        sz = pWal->hdr.szPage;
+        sz = (sz&0xfe00) + ((sz&0x0001)<<16);
+        iOffset = walFrameOffset(iSlot, sz) + WAL_FRAME_HDRSIZE + 40;
+        rc = sqlite3OsRead(pWal->pWalFd, aNew, sizeof(aNew), iOffset);
+        if( rc==SQLITE_OK && memcmp(aOld, aNew, sizeof(aNew)) ){
+          /* TODO: New error code? SQLITE_BUSY_SCHEMA. */
+          rc = SQLITE_BUSY_SNAPSHOT;
+        }
+      }
+    }
+  }
+
+  return rc;
+}
+
+/*
+** The caller holds the WRITER lock. This function returns true if a snapshot
+** upgrade is required before the transaction can be committed, or false
+** otherwise.
+*/
+int sqlite3WalCommitRequiresUpgrade(Wal *pWal){
+  assert( pWal->writeLock );
+  return memcmp(&pWal->hdr, (void*)walIndexHdr(pWal), sizeof(WalIndexHdr))!=0;
+}
+
 /*
 ** End a write transaction.  The commit has already been done.  This
 ** routine merely releases the lock.
@@ -2564,7 +2672,7 @@ int sqlite3WalEndWriteTransaction(Wal *pWal){
 */
 int sqlite3WalUndo(Wal *pWal, int (*xUndo)(void *, Pgno), void *pUndoCtx){
   int rc = SQLITE_OK;
-  if( ALWAYS(pWal->writeLock) ){
+  if( pWal->writeLock ){
     Pgno iMax = pWal->hdr.mxFrame;
     Pgno iFrame;
   
@@ -2603,7 +2711,7 @@ int sqlite3WalUndo(Wal *pWal, int (*xUndo)(void *, Pgno), void *pUndoCtx){
 ** point in the event of a savepoint rollback (via WalSavepointUndo()).
 */
 void sqlite3WalSavepoint(Wal *pWal, u32 *aWalData){
-  assert( pWal->writeLock );
+  /* assert( pWal->writeLock ); */
   aWalData[0] = pWal->hdr.mxFrame;
   aWalData[1] = pWal->hdr.aFrameCksum[0];
   aWalData[2] = pWal->hdr.aFrameCksum[1];
@@ -2619,7 +2727,7 @@ void sqlite3WalSavepoint(Wal *pWal, u32 *aWalData){
 int sqlite3WalSavepointUndo(Wal *pWal, u32 *aWalData){
   int rc = SQLITE_OK;
 
-  assert( pWal->writeLock );
+  assert( pWal->writeLock || aWalData[0]==pWal->hdr.mxFrame );
   assert( aWalData[3]!=pWal->nCkpt || aWalData[0]<=pWal->hdr.mxFrame );
 
   if( aWalData[3]!=pWal->nCkpt ){
@@ -2783,6 +2891,7 @@ int sqlite3WalFrames(
   int szFrame;                    /* The size of a single frame */
   i64 iOffset;                    /* Next byte to write in WAL file */
   WalWriter w;                    /* The writer */
+  int bUpgrade = 0;               /* True if commit requires snapshot upgrade */
 
   assert( pList );
   assert( pWal->writeLock );
@@ -2798,6 +2907,22 @@ int sqlite3WalFrames(
   }
 #endif
 
+  if( isCommit ){
+    volatile WalIndexHdr *pHead = walIndexHdr(pWal);
+    if( pHead->mxFrame>pWal->hdr.mxFrame ){
+      if( memcmp((void*)&pHead[0], (void*)&pHead[1], sizeof(WalIndexHdr))!=0 ){
+        /* TODO: Deal with this case. It's quite possible, but fiddly. */
+        return SQLITE_CORRUPT_BKPT;
+      }
+      memcpy(&pWal->hdr, (void*)pHead, sizeof(WalIndexHdr));
+      if( nTruncate<pWal->hdr.nPage ){
+        /* Do not truncate the database file in this case */
+        nTruncate = pWal->hdr.nPage;
+      }
+      bUpgrade = 1;
+    }
+  }
+
   /* See if it is possible to write these frames into the start of the
   ** log file, instead of appending to it at pWal->hdr.mxFrame.
   */
@@ -2946,6 +3071,14 @@ int sqlite3WalFrames(
     }
   }
 
+  if( rc==SQLITE_OK && bUpgrade ){
+    /* If this commit required a snapshot upgrade, the pager cache is 
+    ** not currently consistent with the head of the wal file. Zeroing
+    ** Wal.hdr here forces the next transaction to reset the cache 
+    ** before beginning to read the db. */
+    memset(&pWal->hdr, 0, sizeof(WalIndexHdr));
+  }
+
   WALTRACE(("WAL%p: frame write %s\n", pWal, rc ? "failed" : "ok"));
   return rc;
 }
@@ -3147,6 +3280,13 @@ int sqlite3WalHeapMemory(Wal *pWal){
   return (pWal && pWal->exclusiveMode==WAL_HEAPMEMORY_MODE );
 }
 
+/*
+** Return true if in a write transaction, false otherwise.
+*/
+int sqlite3WalIsInTrans(Wal *pWal){
+  return (int)pWal->writeLock;
+}
+
 #ifdef SQLITE_ENABLE_ZIPVFS
 /*
 ** If the argument is not NULL, it points to a Wal object that holds a
index 092546354b34c0ab3d20056a7f8a8a52e4d0b5f5..db341caa044eec1fe2c3c4f9c6e3d0e510a83646 100644 (file)
--- a/src/wal.h
+++ b/src/wal.h
@@ -126,6 +126,11 @@ int sqlite3WalExclusiveMode(Wal *pWal, int op);
 */
 int sqlite3WalHeapMemory(Wal *pWal);
 
+/* Return true if the WRITER lock is held. False otherwise. */
+int sqlite3WalIsInTrans(Wal *pWal);
+int sqlite3WalLockForCommit(Wal *pWal, PgHdr *pDirtyList, PgHdr *pPage1); 
+int sqlite3WalCommitRequiresUpgrade(Wal *pWal);
+
 #ifdef SQLITE_ENABLE_ZIPVFS
 /* If the WAL file is not empty, return the number of bytes of content
 ** stored in each frame (i.e. the db page-size when the WAL was created).
diff --git a/test/unlocked.test b/test/unlocked.test
new file mode 100644 (file)
index 0000000..e5f7868
--- /dev/null
@@ -0,0 +1,307 @@
+# 2015 July 26
+#
+# 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.
+#
+#***********************************************************************
+#
+
+set testdir [file dirname $argv0]
+source $testdir/tester.tcl
+source $testdir/lock_common.tcl
+set ::testprefix unlocked
+
+
+do_execsql_test 1.0 {
+  PRAGMA journal_mode = wal;
+} {wal}
+
+do_execsql_test 1.1 {
+  CREATE TABLE t1(k INTEGER PRIMARY KEY, v);
+  BEGIN UNLOCKED;
+    INSERT INTO t1 VALUES(1, 'abcd');
+  COMMIT;
+}
+
+do_execsql_test 1.2 {
+  SELECT * FROM t1;
+} {1 abcd}
+
+do_execsql_test 1.3 {
+  BEGIN UNLOCKED;
+    INSERT INTO t1 VALUES(2, 'efgh');
+  ROLLBACK;
+}
+
+do_execsql_test 1.4 {
+  SELECT * FROM t1;
+} {1 abcd}
+
+
+#-------------------------------------------------------------------------
+# UNLOCKED transactions cannot do cache spills.
+#
+foreach {tn trans spill} {
+  1 {BEGIN UNLOCKED}  0
+  2 {BEGIN}           1
+} {
+  do_test 1.5.$tn {
+    sqlite3 db2 test.db
+    set walsz [file size test.db-wal]
+
+    execsql { PRAGMA cache_size = 10 } db2
+    execsql $trans db2
+    execsql {
+      WITH cnt(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM cnt WHERE i<50)
+        INSERT INTO t1(v) SELECT randomblob(900) FROM cnt;
+    } db2
+
+    expr {[file size test.db-wal]==$walsz}
+  } [expr !$spill]
+
+  execsql ROLLBACK db2
+  db2 close
+}
+
+#-------------------------------------------------------------------------
+# UNLOCKED transactions man not be committed while there are active
+# readers.
+do_execsql_test 1.6.setup {
+  DROP TABLE t1;
+  CREATE TABLE t1(a, b);
+  INSERT INTO t1 VALUES(1, 2);
+  INSERT INTO t1 VALUES(3, 4);
+  INSERT INTO t1 VALUES(5, 6);
+}
+foreach {tn trans commit_ok} {
+  1 {BEGIN UNLOCKED}  0
+  2 {BEGIN}           1
+} {
+  do_test 1.6.$tn.1 {
+    set stmt [sqlite3_prepare db "SELECT * FROM t1" -1 dummy]
+    sqlite3_step $stmt
+  } SQLITE_ROW
+  do_test 1.6.$tn.2 {
+    execsql $trans
+    execsql { INSERT INTO t1 VALUES(7, 8) }
+  } {}
+
+  if { $commit_ok } {
+    do_test 1.6.$tn.3 { catchsql COMMIT } {0 {}}
+  } else {
+    do_test 1.6.$tn.4 { catchsql COMMIT } {/1 {cannot commit transaction .*}/}
+  }
+
+  sqlite3_finalize $stmt
+  catchsql ROLLBACK
+}
+
+#-------------------------------------------------------------------------
+# UNLOCKED transactions may not modify the db schema.
+#
+foreach {tn sql} {
+  1 { CREATE TABLE xx(a, b) }
+  2 { DROP TABLE t1 }
+} {
+  do_catchsql_test 1.7.$tn.1 "
+    BEGIN UNLOCKED;
+    $sql
+  " {1 {cannot modify database schema - UNLOCKED transaction}}
+
+  do_execsql_test 1.7.$tn.2 ROLLBACK
+}
+
+
+do_multiclient_test tn {
+
+  #-----------------------------------------------------------------------
+  # 1. Start an UNLOCKED transaction using [db1].
+  #
+  # 2. Start and then rollback a regular transaction using [db2]. This 
+  #    can be done as the ongoing [db1] transaction is UNLOCKED.
+  #
+  # 3. The [db1] transaction can now be committed, as [db2] has relinquished
+  #    the write lock.
+  #
+  do_test 2.$tn.1.1 {
+    sql1 { 
+      PRAGMA journal_mode = wal;
+      CREATE TABLE t1(k INTEGER PRIMARY KEY, v);
+      INSERT INTO t1 VALUES(1, 'one');
+    }
+    sql1 { 
+      BEGIN UNLOCKED;
+        INSERT INTO t1 VALUES(2, 'two');
+    }
+    code1 { sqlite3_get_autocommit db }
+  } 0
+
+  do_test 2.$tn.1.2 {
+    sql2 {
+      BEGIN;
+        INSERT INTO t1 VALUES(3, 'three');
+      ROLLBACK;
+    }
+  } {}
+
+  do_test 2.$tn.1.3 {
+    sql1 COMMIT
+    sql2 { SELECT * FROM t1 }
+  } {1 one 2 two}
+  
+  #-----------------------------------------------------------------------
+  # 1. Start an UNLOCKED transaction using [db1].
+  #
+  # 2. Commit a transaction using [db2].
+  #
+  # 3. Try to commit with [db1]. Check that SQLITE_BUSY_SNAPSHOT is returned,
+  #    and the transaction is not rolled back.
+  #
+  do_test 2.$tn.2.1 {
+    sql1 {
+      BEGIN UNLOCKED;
+        INSERT INTO t1 VALUES(-1, 'hello world');
+    }
+  } {}
+
+  do_test 2.$tn.2.2 {
+    sql2 {
+      INSERT INTO t1 VALUES(3, 'three');
+    }
+  } {}
+
+  do_test 2.$tn.2.3.1 {
+    set rc [catch { sql1 COMMIT } msg]
+    list $rc $msg
+  } {1 {database is locked}}
+
+  do_test 2.$tn.2.3.2 {
+    code1 { list [sqlite3_extended_errcode db] [sqlite3_get_autocommit db] }
+  } {SQLITE_BUSY_SNAPSHOT 0}
+
+  do_test 2.$tn.2.3.3 {
+    sql1 {
+      SELECT * FROM t1;
+      ROLLBACK;
+    }
+  } {-1 {hello world} 1 one 2 two}
+  
+  #-----------------------------------------------------------------------
+  # 1. Start an UNLOCKED transaction using [db1].
+  #
+  # 2. Open a transaction using [db2].
+  #
+  # 3. Try to commit with [db1]. Check that SQLITE_BUSY is returned,
+  #    and the transaction is not rolled back.
+  #
+  # 4. Have [db2] roll its transaction back. Then check that [db1] can
+  #    commit.
+  #
+  do_test 2.$tn.3.1 {
+    sql1 {
+      BEGIN UNLOCKED;
+        INSERT INTO t1 VALUES(4, 'four');
+    }
+  } {}
+
+  do_test 2.$tn.3.2 {
+    sql2 {
+      BEGIN;
+        INSERT INTO t1 VALUES(-1, 'xyz');
+    }
+  } {}
+
+  do_test 2.$tn.3.3.1 {
+    set rc [catch { sql1 COMMIT } msg]
+    list $rc $msg
+  } {1 {database is locked}}
+
+  do_test 2.$tn.3.3.2 {
+    code1 { list [sqlite3_extended_errcode db] [sqlite3_get_autocommit db] }
+  } {SQLITE_BUSY 0}
+
+  do_test 2.$tn.3.3.3 {
+    sql1 { SELECT * FROM t1; }
+  } {1 one 2 two 3 three 4 four}
+
+  do_test 2.$tn.3.4 {
+    sql2 ROLLBACK
+    sql1 COMMIT
+    sql1 { SELECT * FROM t1; }
+  } {1 one 2 two 3 three 4 four}
+
+  #-----------------------------------------------------------------------
+  # 1. Create a second table - t2.
+  #
+  # 2. Write to t1 with [db] and t2 with [db2].
+  #
+  # 3. See if it worked.
+  #
+  do_test 2.$tn.4.1 {
+    sql1 { CREATE TABLE t2(a, b) }
+  } {}
+  do_test 2.$tn.4.2 {
+    sql2 {
+      BEGIN UNLOCKED;
+        INSERT INTO t2 VALUES('i', 'n');
+    }
+
+    sql1 {
+      BEGIN UNLOCKED;
+        INSERT INTO t1 VALUES(5, 'five');
+      COMMIT;
+    }
+
+    sql2 COMMIT
+  } {}
+
+  do_test 2.$tn.4.3.1 {
+    sql2 {SELECT * FROM t1}
+  } {1 one 2 two 3 three 4 four 5 five}
+  do_test 2.$tn.4.3.2 {
+    sql1 {SELECT * FROM t1}
+  } {1 one 2 two 3 three 4 four 5 five}
+
+  do_test 2.$tn.4.3.3 { sql2 {SELECT * FROM t2} } {i n}
+  do_test 2.$tn.4.3.4 { sql1 {SELECT * FROM t2} } {i n}
+
+  #-----------------------------------------------------------------------
+  # The "schema cookie" issue.
+  #
+  # 1. Begin and UNLOCKED write to "t1" using [db]
+  #
+  # 2. Create an index on t1 using [db2].
+  #
+  # 3. Attempt to commit the UNLOCKED write. This is an SQLITE_BUSY_SNAPSHOT,
+  #    even though there is no page collision.
+  #
+  
+  do_test 2.$tn.5.1 {
+    sql1 {
+      BEGIN UNLOCKED;
+        INSERT INTO t1 VALUES(6, 'six');
+    }
+  } {}
+
+  do_test 2.$tn.5.2 {
+    sql2 { CREATE INDEX i1 ON t1(v); }
+  } {}
+
+  do_test 2.$tn.5.3 {
+    list [catch { sql1 { COMMIT } } msg] $msg [sqlite3_errcode db]
+  } {1 {database is locked} SQLITE_BUSY_SNAPSHOT}
+
+  do_test 2.$tn.5.4 {
+    sql2 { PRAGMA integrity_check }
+  } {ok}
+  catch { sql1 ROLLBACK }
+
+}
+
+
+
+finish_test
index 721611f5a36ce8e00b1a106348a17b9dff532c92..d1810cbeb99f2be7ac8abcd921f8ce17f211f94f 100644 (file)
@@ -262,6 +262,7 @@ static Keyword aKeywordTable[] = {
   { "TRIGGER",          "TK_TRIGGER",      TRIGGER                },
   { "UNION",            "TK_UNION",        COMPOUND               },
   { "UNIQUE",           "TK_UNIQUE",       ALWAYS                 },
+  { "UNLOCKED",         "TK_UNLOCKED",     ALWAYS                 },
   { "UPDATE",           "TK_UPDATE",       ALWAYS                 },
   { "USING",            "TK_USING",        ALWAYS                 },
   { "VACUUM",           "TK_VACUUM",       VACUUM                 },