From d095b2c3d58d166f19fa3c773e593cba723cdfb8 Mon Sep 17 00:00:00 2001 From: dan Date: Mon, 19 Jun 2023 18:16:19 +0000 Subject: [PATCH] Add the sqlite3_commit_status() API. For querying a connection for (a) the ranges of wal/wal2 frames written after a successful commit, and (b) the conflicting frame following a failed commit of a BEGIN CONCURRENT transaction. FossilOrigin-Name: 4b08d4dad6b254a342353e3f765066c85cbc5450fe13501665c648627cca21cd --- manifest | 31 ++--- manifest.uuid | 2 +- src/btree.c | 70 +++++++++- src/btreeInt.h | 6 + src/pager.c | 4 +- src/pager.h | 2 +- src/sqlite.h.in | 92 +++++++++++++ src/sqliteInt.h | 6 + src/test1.c | 52 ++++++++ src/wal.c | 50 ++++++- src/wal.h | 2 +- test/commitstatus.test | 297 +++++++++++++++++++++++++++++++++++++++++ 12 files changed, 581 insertions(+), 33 deletions(-) create mode 100644 test/commitstatus.test diff --git a/manifest b/manifest index a268b1ce86..0d0e067bbe 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Merge\sall\s3.42.0\srelease\schanges\sinto\sthe\sbedrock\sbranch. -D 2023-05-16T13:04:38.652 +C Add\sthe\ssqlite3_commit_status()\sAPI.\sFor\squerying\sa\sconnection\sfor\s(a)\sthe\sranges\sof\swal/wal2\sframes\swritten\safter\sa\ssuccessful\scommit,\sand\s(b)\sthe\sconflicting\sframe\sfollowing\sa\sfailed\scommit\sof\sa\sBEGIN\sCONCURRENT\stransaction. +D 2023-06-19T18:16:19.942 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -583,9 +583,9 @@ F src/auth.c f4fa91b6a90bbc8e0d0f738aa284551739c9543a367071f55574681e0f24f8cf F src/backup.c 5c97e8023aab1ce14a42387eb3ae00ba5a0644569e3476f38661fa6f824c3523 F src/bitvec.c 3907fcbe8a0c8c2db58d97087d15cdabbf2842adb9125df9ab9ff87d3db16775 F src/btmutex.c 6ffb0a22c19e2f9110be0964d0731d2ef1c67b5f7fabfbaeb7b9dabc4b7740ca -F src/btree.c b665847a53bc556d663a70231fdaa600f5eb4d7d29684690fd4f63b1262621b3 +F src/btree.c a1e13541545e5dd916daadb1a7730cd415ca3c8e25e2bdee73b4ee149000f77c F src/btree.h 77a092acf63526827e74e88d0480123212d079593a841ff1fe85507adf256ef6 -F src/btreeInt.h 757425aeff908b819f2f086eadcc44ca847a672617ced5161c56c60c6b39c226 +F src/btreeInt.h a2c8d4894939eb7f31be5813109304e47147b5fedc3ed9e870a34ab534631ea3 F src/build.c 52784bddd510438361a3ab1141db6aaf0aad76096e2e06208e3c23d21b279ba2 F src/callback.c db3a45e376deff6a16c0058163fe0ae2b73a2945f3f408ca32cf74960b28d490 F src/complete.c a3634ab1e687055cd002e11b8f43eb75c17da23e @@ -631,8 +631,8 @@ F src/os_setup.h 6011ad7af5db4e05155f385eb3a9b4470688de6f65d6166b8956e58a3d87210 F src/os_unix.c a585801080e5d36365a409221813534216f503b58f1f7a4398f225c4ae0bc424 F src/os_win.c 2b2411279f7b24f927591561303fc5871845732df42641cbf695c23640b16975 F src/os_win.h 7b073010f1451abe501be30d12f6bc599824944a -F src/pager.c 9dd0da3b43166ceb465fce53a0826b0f6c3ea6ea9e294f7ce22a97d2f6d1f8e8 -F src/pager.h 3ddab454e313da7c93f92fea35c842ad17ae9f4e96254871ddb0171b2bfb859a +F src/pager.c d41c9e4c5804de4acf00036bb26cd41a798341a051801b08b98dee73fb4999ec +F src/pager.h e055e649d93f1e121ce50b30a3d01a5225e6d2c45d712c676c8477dec19beeb8 F src/parse.y 03d4d7a079481e6fab9f9256971fa87c716af20d46fffba2ecea21583e6f05db F src/pcache.c 8ee13acccfd9accbf0af94910b7323dd7f7d55300d92ddafcf40e34fcc8e21be F src/pcache.h 1497ce1b823cf00094bb0cf3bac37b345937e6f910890c626b16512316d3abf5 @@ -646,15 +646,15 @@ F src/resolve.c 3e53e02ce87c9582bd7e7d22f13f4094a271678d9dc72820fa257a2abb5e4032 F src/rowset.c ba9515a922af32abe1f7d39406b9d35730ed65efab9443dc5702693b60854c92 F src/select.c ead092c26ba1c6d525fc6319923581a34a0b6ae28d05bb3196ed0106b69c37d4 F src/shell.c.in 52836b4002a2cad8095b451f0c39a6542c23a231eb0ed5e39387bc8b1f7aaa9e -F src/sqlite.h.in bd6db733c0f8559c5c28e14c406c27a28635d47c09bd9e0c2778ea1d30a291ab +F src/sqlite.h.in 12f208a569d0340814b52e50c3e30a43fc32b5ff24c4b9854edb85fa555f5196 F src/sqlite3.rc 5121c9e10c3964d5755191c80dd1180c122fc3a8 F src/sqlite3ext.h da473ce2b3d0ae407a6300c4a164589b9a6bfdbec9462688a8593ff16f3bb6e4 -F src/sqliteInt.h 5a6aac5ca272df3dfc3792dafbee86e417c82cfbc59a040349bfcf88504559b8 +F src/sqliteInt.h d52ede4c29dd1af380f882f29fc0209dc0f20439e953ac349a417ca0f4ed5661 F src/sqliteLimit.h d7323ffea5208c6af2734574bae933ca8ed2ab728083caa117c9738581a31657 F src/status.c 160c445d7d28c984a0eae38c144f6419311ed3eace59b44ac6dafc20db4af749 F src/table.c 0f141b58a16de7e2fbe81c308379e7279f4c6b50eb08efeec5892794a0ba30d1 F src/tclsqlite.c 8522a04fb9c84faa1d80354430ae0ee9349727a3a4b32e3cfe39b9be8324cabd -F src/test1.c fce757d6c5cc7ecf010283fabe674cb021c1ce3f874803b21c978e6c768138e1 +F src/test1.c 2b1cee78d45a95b08cf7c7aded6072c65401b73e69debec4ded579a034dbf772 F src/test2.c 827446e259a3b7ab949da1542953edda7b5117982576d3e6f1c24a0dd20a5cef F src/test3.c 61798bb0d38b915067a8c8e03f5a534b431181f802659a6616f9b4ff7d872644 F src/test4.c 4533b76419e7feb41b40582554663ed3cd77aaa54e135cf76b3205098cd6e664 @@ -726,8 +726,8 @@ F src/vdbetrace.c fe0bc29ebd4e02c8bc5c1945f1d2e6be5927ec12c06d89b03ef2a4def34bf8 F src/vdbevtab.c aae4bd769410eb7e1d02c42613eec961d514459b1c3c1c63cfc84e92a137daac F src/vtab.c 4758a96d36c9a120848386ae603b1ab32a4876e0a1faf81bfcfb524455e583dc F src/vxworks.h d2988f4e5a61a4dfe82c6524dd3d6e4f2ce3cdb9 -F src/wal.c b6c4ad0a5689dbba6f339cff30b8fb029b092db81544821cf05f670368eb5ff8 -F src/wal.h 7a733af13b966ecb81872ce397e862116b3575ea53245b90b139a2873ee87825 +F src/wal.c e78a2f4af580db963f987761922a79fa40bc466b37c203ceeb30034b89e29df3 +F src/wal.h dd2cd9880f308a1bda0a72f36a29c3d4388d47db45f321ebe936a378ac845e32 F src/walker.c f890a3298418d7cba3b69b8803594fdc484ea241206a8dfa99db6dd36f8cbb3b F src/where.c b74a83b4c8f65b218c5c1c8d9122433f85ee1300fd9263ba1697d0e1040eeb36 F src/whereInt.h e25203e5bfee149f5f1225ae0166cfb4f1e65490c998a024249e98bb0647377c @@ -887,6 +887,7 @@ F test/collateB.test 1e68906951b846570f29f20102ed91d29e634854ee47454d725f2151eca F test/colmeta.test 2c765ea61ee37bc43bbe6d6047f89004e6508eb1 F test/colname.test 87ad5458bb8709312dac0d6755fd30e8e4ca83298d0a9ef6e5c24277a3c3390e F test/columncount.test 6fe99c2f35738b0129357a1cf3fa483f76140f4cd8a89014c88c33c876d2638f +F test/commitstatus.test d5a871506ce5944a29afb7e65ce47ca7f76cadc1d09775022830258fdd6168a1 F test/concfault.test 500f17c3fcfe7705114422bcc6ddd3c740001a43 F test/concfault2.test 34b3fd258836aa305475d00e804c7450ade92f0de0bf9fa620e701446669bb12 F test/concurrent.test a0248ec6e3e79a5948453649cf86b5b359175cba55ea636b15426d6f0fa6c3da @@ -2106,8 +2107,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 -P d55ba8bb85b1796d534d2db196db63312624630738712054553bd01f2ab30df9 137057f95778b3c913854d2182d0fbbfd9dd117db5566dabb5a22d927a59de62 -R 12df87a4c2fba63aa28bed03bc214887 -U drh -Z 82ddbc998d903aef55929bc948a95756 +P 1348c2a59027d6e260d4c7543479093eaf7baccaa3ea61a0d091c44e0dea4b8c +R c8cdea1b9aa49524702d53e388aa9458 +U dan +Z 69c8dd77f80c6e6120460e3d71577e65 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 321632d167..6ea029c061 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -1348c2a59027d6e260d4c7543479093eaf7baccaa3ea61a0d091c44e0dea4b8c \ No newline at end of file +4b08d4dad6b254a342353e3f765066c85cbc5450fe13501665c648627cca21cd \ No newline at end of file diff --git a/src/btree.c b/src/btree.c index 30b3ffb657..2022f4bc31 100644 --- a/src/btree.c +++ b/src/btree.c @@ -4654,6 +4654,9 @@ int sqlite3BtreeCommitPhaseOne(Btree *p, const char *zSuperJrnl){ BtShared *pBt = p->pBt; sqlite3BtreeEnter(p); +#ifndef SQLITE_OMIT_CONCURRENT + memset(p->aCommit, 0, sizeof(p->aCommit)); +#endif #ifndef SQLITE_OMIT_AUTOVACUUM if( pBt->autoVacuum ){ assert( ISCONCURRENT==0 ); @@ -4673,6 +4676,19 @@ int sqlite3BtreeCommitPhaseOne(Btree *p, const char *zSuperJrnl){ if( rc==SQLITE_OK ){ rc = sqlite3PagerCommitPhaseOne(pBt->pPager, zSuperJrnl, 0); } +#ifndef SQLITE_OMIT_CONCURRENT + if( rc==SQLITE_OK ){ + u32 iPrev = 0; + u32 iCurrent = 0; + sqlite3PagerWalInfo(pBt->pPager, &iPrev, &iCurrent); + if( (iPrev&0x80000000)!=(iCurrent&0x80000000) ){ + iPrev = (iPrev & 0x7FFFFFFF) | (iCurrent & 0x80000000); + } + + p->aCommit[SQLITE_COMMIT_FIRSTFRAME] = iPrev+1; + p->aCommit[SQLITE_COMMIT_NFRAME] = iCurrent-iPrev; + } +#endif sqlite3BtreeLeave(p); } return rc; @@ -11802,22 +11818,29 @@ int sqlite3HeaderSizeBtree(void){ return ROUND8(sizeof(MemPage)); } ** is returned. */ int sqlite3BtreeExclusiveLock(Btree *p){ + sqlite3 *db = p->db; int rc; Pgno pgno = 0; BtShared *pBt = p->pBt; assert( p->inTrans==TRANS_WRITE && pBt->pPage1 ); + memset(db->aCommit, 0, sizeof(db->aCommit)); sqlite3BtreeEnter(p); rc = sqlite3PagerExclusiveLock(pBt->pPager, - (p->db->eConcurrent==CONCURRENT_SCHEMA) ? 0 : pBt->pPage1->pDbPage, - &pgno + (db->eConcurrent==CONCURRENT_SCHEMA) ? 0 : pBt->pPage1->pDbPage, + db->aCommit ); #ifdef SQLITE_OMIT_CONCURRENT - assert( pgno==0 ); + assert( db->aCommit[SQLITE_COMMIT_CONFLICT_PGNO]==0 ); #else - if( rc==SQLITE_BUSY_SNAPSHOT && pgno ){ + if( (rc==SQLITE_BUSY_SNAPSHOT) + && (pgno = db->aCommit[SQLITE_COMMIT_CONFLICT_PGNO]) + ){ + int iDb; PgHdr *pPg = 0; - int rc2 = sqlite3PagerGet(pBt->pPager, pgno, &pPg, 0); - if( rc2==SQLITE_OK ){ + for(iDb=0; db->aDb[iDb].pBt!=p; iDb++); + db->aCommit[SQLITE_COMMIT_CONFLICT_DB] = (u32)iDb; + (void)sqlite3PagerGet(pBt->pPager, pgno, &pPg, 0); + if( pPg ){ int bWrite = -1; const char *zObj = 0; const char *zTab = 0; @@ -11909,3 +11932,38 @@ int sqlite3BtreeConnectionCount(Btree *p){ return p->pBt->nRef; } #endif + +/* +** Access details of recent COMMIT commands. This function allows various +** details related to the most recent COMMIT command to be accessed. +** The requested value is always returned via output parameter (*piVal). +** The specific value requested is identified by parameter op (see +** below). +** +** SQLITE_OK is returned if successful, or SQLITE_ERROR if the "op" or +** "zDb" paramters are unrecognized. +*/ +int sqlite3_commit_status( + sqlite3 *db, /* Database handle */ + const char *zDb, /* Name of database - "main" etc. */ + int op, /* SQLITE_COMMIT_XXX constant */ + unsigned int *piVal /* OUT: Write requested value here */ +){ + int rc = SQLITE_OK; +#ifndef SQLITE_OMIT_CONCURRENT + if( op<0 || op>SQLITE_COMMIT_CONFLICT_PGNO ){ + rc = SQLITE_ERROR; + }else if( op==SQLITE_COMMIT_FIRSTFRAME || op==SQLITE_COMMIT_NFRAME ){ + int iDb = sqlite3FindDbName(db, zDb); + if( iDb<0 ){ + rc = SQLITE_ERROR; + }else{ + *piVal = db->aDb[iDb].pBt->aCommit[op]; + } + }else{ + *piVal = db->aCommit[op]; + } +#endif + return rc; +} + diff --git a/src/btreeInt.h b/src/btreeInt.h index e48d69b3db..194ff72eb7 100644 --- a/src/btreeInt.h +++ b/src/btreeInt.h @@ -364,6 +364,12 @@ struct Btree { #ifndef SQLITE_OMIT_SHARED_CACHE BtLock lock; /* Object used to lock page 1 */ #endif +#ifndef SQLITE_OMIT_CONCURRENT + /* Return values for sqlite3_commit_status() requests: + ** SQLITE_COMMIT_FIRSTFRAME, COMMIT_NFRAME. + */ + u32 aCommit[2]; +#endif }; /* diff --git a/src/pager.c b/src/pager.c index 6681793d0b..917856670b 100644 --- a/src/pager.c +++ b/src/pager.c @@ -6437,7 +6437,7 @@ int sqlite3PagerSync(Pager *pPager, const char *zSuper){ ** is returned. Otherwise, if some other error occurs (IO error, OOM etc.), ** and SQLite error code is returned. */ -int sqlite3PagerExclusiveLock(Pager *pPager, PgHdr *pPage1, Pgno *piConflict){ +int sqlite3PagerExclusiveLock(Pager *pPager, PgHdr *pPage1, u32 *aConflict){ int rc = pPager->errCode; assert( assert_pager_state(pPager) ); if( rc==SQLITE_OK ){ @@ -6458,7 +6458,7 @@ int sqlite3PagerExclusiveLock(Pager *pPager, PgHdr *pPage1, Pgno *piConflict){ ** non-zero. */ do { rc = sqlite3WalLockForCommit( - pPager->pWal, pPage1, pPager->pAllRead, piConflict + pPager->pWal, pPage1, pPager->pAllRead, aConflict ); }while( rc==SQLITE_BUSY && pPager->xBusyHandler(pPager->pBusyHandlerArg) diff --git a/src/pager.h b/src/pager.h index 58bfc0fa19..2ec6f1ac41 100644 --- a/src/pager.h +++ b/src/pager.h @@ -180,7 +180,7 @@ void *sqlite3PagerGetExtra(DbPage *); void sqlite3PagerPagecount(Pager*, int*); int sqlite3PagerBegin(Pager*, int exFlag, int); int sqlite3PagerCommitPhaseOne(Pager*,const char *zSuper, int); -int sqlite3PagerExclusiveLock(Pager*, DbPage *pPage1, Pgno*); +int sqlite3PagerExclusiveLock(Pager*, DbPage *pPage1, u32*); int sqlite3PagerSync(Pager *pPager, const char *zSuper); int sqlite3PagerCommitPhaseTwo(Pager*); int sqlite3PagerRollback(Pager*); diff --git a/src/sqlite.h.in b/src/sqlite.h.in index c6fbaf8d36..e49076f84c 100644 --- a/src/sqlite.h.in +++ b/src/sqlite.h.in @@ -10619,6 +10619,98 @@ int sqlite3_deserialize( #define SQLITE_DESERIALIZE_RESIZEABLE 2 /* Resize using sqlite3_realloc64() */ #define SQLITE_DESERIALIZE_READONLY 4 /* Database is read-only */ +/* +** Access details of recent COMMIT commands. This function allows various +** details related to the most recent COMMIT command to be accessed. +** The requested value is always returned via output parameter (*piVal). +** The specific value requested is identified by parameter op (see +** below). +** +** SQLITE_OK is returned if successful, or SQLITE_ERROR if the "op" or +** "zDb" paramters are unrecognized. +*/ +int sqlite3_commit_status( + sqlite3 *db, /* Database handle */ + const char *zDb, /* Name of database - "main" etc. */ + int op, /* SQLITE_COMMIT_XXX constant */ + unsigned int *piVal /* OUT: Write requested value here */ +); + +/* +** The following describes the five requests supported by +** sqlite3_commit_status(), each identified by an SQLITE_COMMIT_XXX +** constant: +** +** SQLITE_COMMIT_FIRSTFRAME: +** In this case argument zDb must be "main", or "temp", or else the name of +** an attached database. If zDb does not correspond to any attached database, +** SQLITE_ERROR is returned. +** +** The final value of (*piVal) for this request is only defined if (a) the +** most recent attempt to write to the database connection was successful, +** (b) the most recent attempt to write to the database did write to database +** zDb, and (c) zDb is a wal mode database. +** +** If the above conditions are true, then output parameter (*piVal) is +** set to the frame number of the first frame written by the recent +** transaction. In wal mode, or in wal2 mode when a transaction is +** written into the *-wal file, the frame number indicates the frame's +** position in the wal file - frames are numbered starting from 1. In +** wal2 mode, when a transaction is written to the *-wal2 file, the frame +** number is the frame's position in the *-wal2 file, plus (1 << 31). +** +** Note: Although the a database may have up to (1<<32) pages, each wal +** file may contain at most (1<<31) frames. +** +** SQLITE_COMMIT_NFRAME: +** zDb is interpreted in the same way as, and the final value of (*piVal) +** is undefined, for SQLITE_COMMIT_FIRSTFRAME. +** +** Otherwise, (*piVal) is set to the number of frames written by the +** recent transaction. +** +** SQLITE_COMMIT_CONFLICT_DB: +** Parameter zDb is ignored for this request. The results of this +** request are only defined if the most recent attempt to write to +** the database handle was a BEGIN CONCURRENT transaction that +** failed with an SQLITE_BUSY_SNAPSHOT error. +** +** In other cases, (*piVal) is set to the index of the database +** on which the SQLITE_BUSY_SNAPSHOT error occurred (0 for main, +** a value of 2 or greater for an attached database). This value +** may be used with the sqlite3_db_name() API to find the name +** of the conflicting database. +** +** SQLITE_COMMIT_CONFLICT_FRAME: +** Parameter zDb is ignored for this request. The results of this +** request are only defined if the most recent attempt to write to +** the database handle was a BEGIN CONCURRENT transaction that +** failed with an SQLITE_BUSY_SNAPSHOT error. +** +** (*piVal) is set to the frame number of the conflicting frame for +** the recent SQLITE_BUSY_SNAPSHOT error. The conflicting transaction may +** be found by comparing this value with the FIRSTFRAME and +** NFRAME values for recent succesfully committed transactions on +** the same db. If the CONFLICT_FRAME value is F, then the conflicting +** transaction is the most recent successful commit for which +** (FIRSTFRAME <= F <= FIRSTFRAME+NFRAME) is true. +** +** SQLITE_COMMIT_CONFLICT_PGNO: +** Parameter zDb is ignored for this request. The results of this +** request are only defined if the previous attempt to write to +** the database using database handle db failed with +** SQLITE_BUSY_SNAPSHOT. +** +** Return the page number of the conflicting page for the most +** recent SQLITE_BUSY_SNAPSHOT error. +*/ +#define SQLITE_COMMIT_FIRSTFRAME 0 +#define SQLITE_COMMIT_NFRAME 1 +#define SQLITE_COMMIT_CONFLICT_DB 2 +#define SQLITE_COMMIT_CONFLICT_FRAME 3 +#define SQLITE_COMMIT_CONFLICT_PGNO 4 + + /* ** Undo the hack that converts floating point types to integer for ** builds on processors without floating point support. diff --git a/src/sqliteInt.h b/src/sqliteInt.h index 5f09cd7567..af111d51c6 100644 --- a/src/sqliteInt.h +++ b/src/sqliteInt.h @@ -1740,6 +1740,12 @@ struct sqlite3 { #ifdef SQLITE_USER_AUTHENTICATION sqlite3_userauth auth; /* User authentication information */ #endif +#ifndef SQLITE_OMIT_CONCURRENT + /* Return values for sqlite3_commit_status() requests: + ** SQLITE_COMMIT_CONFLICT_DB, CONFLICT_FRAME and CONFLICT_PGNO. + */ + u32 aCommit[5]; +#endif }; /* diff --git a/src/test1.c b/src/test1.c index 8b0592b10d..720da685ca 100644 --- a/src/test1.c +++ b/src/test1.c @@ -2416,6 +2416,57 @@ static int SQLITE_TCLAPI test_create_null_module( } #endif /* SQLITE_OMIT_VIRTUALTABLE */ +/* +** tclcmd: sqlite3_commit_status db DBNAME OP +*/ +static int SQLITE_TCLAPI test_commit_status( + ClientData clientData, /* Unused */ + Tcl_Interp *interp, /* The TCL interpreter that invoked this command */ + int objc, /* Number of arguments */ + Tcl_Obj *CONST objv[] /* Command arguments */ +){ + struct Op { + const char *zOp; + int op; + } aOp[] = { + { "FIRSTFRAME", SQLITE_COMMIT_FIRSTFRAME }, + { "NFRAME", SQLITE_COMMIT_NFRAME }, + { "CONFLICT_DB", SQLITE_COMMIT_CONFLICT_DB }, + { "CONFLICT_FRAME", SQLITE_COMMIT_CONFLICT_FRAME }, + { "CONFLICT_PGNO", SQLITE_COMMIT_CONFLICT_PGNO }, + { 0, 0 } + }; + sqlite3 *db = 0; + const char *zDb = 0; + int op = 0; + int rc = SQLITE_OK; + unsigned int val = 0; + + if( objc!=4 ){ + Tcl_WrongNumArgs(interp, 1, objv, "DB DBNAME OP"); + return TCL_ERROR; + } + if( getDbPointer(interp, Tcl_GetString(objv[1]), &db) ){ + return TCL_ERROR; + } + zDb = Tcl_GetString(objv[2]); + if( Tcl_GetIndexFromObjStruct( + interp, objv[3], aOp, sizeof(aOp[0]), "OP", 0, &op + )){ + return TCL_ERROR; + } + op = aOp[op].op; + + rc = sqlite3_commit_status(db, zDb, op, &val); + if( rc==SQLITE_OK ){ + Tcl_SetObjResult(interp, Tcl_NewWideIntObj((i64)val)); + return TCL_OK; + } + + Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1)); + return TCL_ERROR; +} + #ifdef SQLITE_ENABLE_SNAPSHOT /* ** Usage: sqlite3_snapshot_get DB DBNAME @@ -9046,6 +9097,7 @@ int Sqlitetest1_Init(Tcl_Interp *interp){ #ifndef SQLITE_OMIT_VIRTUALTABLE { "create_null_module", test_create_null_module, 0 }, #endif + { "sqlite3_commit_status", test_commit_status, 0 }, }; static int bitmask_size = sizeof(Bitmask)*8; static int longdouble_size = sizeof(LONGDOUBLE_TYPE); diff --git a/src/wal.c b/src/wal.c index c7fe1f7791..c19713ab17 100644 --- a/src/wal.c +++ b/src/wal.c @@ -3683,6 +3683,15 @@ int sqlite3WalSnapshotRecover(Wal *pWal){ } #endif /* SQLITE_ENABLE_SNAPSHOT */ +/* +** Return the current last frame in the wal-index - mxFrame for *-wal, +** or mxFrame2 for *-wal2. If the last frame is current in wal2, return +** mxFrame2 without clearing the 0x80000000 bit. +*/ +static u32 walGetPriorFrame(WalIndexHdr *pHdr){ + return (walidxGetFile(pHdr) ? pHdr->mxFrame2 : pHdr->mxFrame); +} + /* ** Begin a read transaction on the database. ** @@ -3745,7 +3754,7 @@ int sqlite3WalBeginReadTransaction(Wal *pWal, int *pChanged){ rc = walOpenWal2(pWal); } - pWal->nPriorFrame = pWal->hdr.mxFrame; + pWal->nPriorFrame = walGetPriorFrame(&pWal->hdr); #ifdef SQLITE_ENABLE_SNAPSHOT if( rc==SQLITE_OK ){ if( pSnapshot && memcmp(pSnapshot, &pWal->hdr, sizeof(WalIndexHdr))!=0 ){ @@ -4149,6 +4158,24 @@ static int walUpgradeReadlock(Wal *pWal){ #ifndef SQLITE_OMIT_CONCURRENT + +/* +** A concurrent transaction has conflicted with external frame iExternal. +** Transform this value to the one required by SQLITE_COMMIT_CONFLICT_FRAME - +** the frame offset within its wal file, with the 0x80000000 bit set for +** wal2, clear for the default wal file. +*/ +static u32 walConflictFrame(Wal *pWal, u32 iExternal){ + u32 iRet = iExternal; + if( isWalMode2(pWal) ){ + int bFile = walExternalDecode(iExternal, &iRet); + iRet = (iRet | (bFile ? 0x80000000 : 0)); + } + return iRet; +} + + + /* ** This function is only ever called when committing a "BEGIN CONCURRENT" ** transaction. It may be assumed that no frames have been written to @@ -4181,7 +4208,7 @@ int sqlite3WalLockForCommit( Wal *pWal, PgHdr *pPg1, Bitvec *pAllRead, - Pgno *piConflict + u32 *aConflict ){ int rc = walWriteLock(pWal); @@ -4213,7 +4240,10 @@ int sqlite3WalLockForCommit( if( pPg1==0 ){ /* If pPg1==0, then the current transaction modified the database ** schema. This means it conflicts with all other transactions. */ - *piConflict = 1; + u32 bFile = walidxGetFile(&pWal->hdr); + u32 iFrame = walidxGetMxFrame(&head, bFile) | (bFile << 31); + aConflict[SQLITE_COMMIT_CONFLICT_PGNO] = 1; + aConflict[SQLITE_COMMIT_CONFLICT_FRAME] = iFrame; rc = SQLITE_BUSY_SNAPSHOT; } @@ -4276,15 +4306,20 @@ int sqlite3WalLockForCommit( if( bWal2 ){ iWal = walExternalDecode(iFrame, &iFrame); } - sz = pWal->hdr.szPage; + sz = head.szPage; sz = (sz&0xfe00) + ((sz&0x0001)<<16); iOff = walFrameOffset(iFrame, sz) + WAL_FRAME_HDRSIZE + 40; rc = sqlite3OsRead(pWal->apWalFd[iWal],aNew,sizeof(aNew),iOff); if( rc==SQLITE_OK && memcmp(aOld, aNew, sizeof(aNew)) ){ + u32 iFrame = walConflictFrame(pWal, sLoc.iZero+i); + aConflict[SQLITE_COMMIT_CONFLICT_PGNO] = 1; + aConflict[SQLITE_COMMIT_CONFLICT_FRAME] = iFrame; rc = SQLITE_BUSY_SNAPSHOT; } }else if( sqlite3BitvecTestNotNull(pAllRead, sLoc.aPgno[i-1]) ){ - *piConflict = sLoc.aPgno[i-1]; + u32 iFrame = walConflictFrame(pWal, sLoc.iZero+i); + aConflict[SQLITE_COMMIT_CONFLICT_PGNO] = sLoc.aPgno[i-1]; + aConflict[SQLITE_COMMIT_CONFLICT_FRAME] = iFrame; rc = SQLITE_BUSY_SNAPSHOT; }else if( (pPg = sqlite3PagerLookup(pPg1->pPager, sLoc.aPgno[i-1])) ){ @@ -4318,7 +4353,7 @@ int sqlite3WalLockForCommit( } } - pWal->nPriorFrame = pWal->hdr.mxFrame; + pWal->nPriorFrame = walGetPriorFrame(&pWal->hdr); return rc; } @@ -4591,6 +4626,7 @@ static int walRestartLog(Wal *pWal){ rc = walUpgradeReadlock(pWal); } + pWal->nPriorFrame = walGetPriorFrame(&pWal->hdr); return rc; } @@ -5346,7 +5382,7 @@ int sqlite3WalInfo(Wal *pWal, u32 *pnPrior, u32 *pnFrame){ int rc = SQLITE_OK; if( pWal ){ *pnPrior = pWal->nPriorFrame; - *pnFrame = walidxGetMxFrame(&pWal->hdr, walidxGetFile(&pWal->hdr)); + *pnFrame = walGetPriorFrame(&pWal->hdr); } return rc; } diff --git a/src/wal.h b/src/wal.h index 137abcba2b..6989142d91 100644 --- a/src/wal.h +++ b/src/wal.h @@ -139,7 +139,7 @@ void sqlite3WalSnapshotUnlock(Wal *pWal); #ifndef SQLITE_OMIT_CONCURRENT /* Tell the wal layer that we want to commit a concurrent transaction */ -int sqlite3WalLockForCommit(Wal *pWal, PgHdr *pPg, Bitvec *pRead, Pgno*); +int sqlite3WalLockForCommit(Wal *pWal, PgHdr *pPg, Bitvec *pRead, u32*); /* Upgrade the state of the client to take into account changes written ** by other connections */ diff --git a/test/commitstatus.test b/test/commitstatus.test new file mode 100644 index 0000000000..92580fe765 --- /dev/null +++ b/test/commitstatus.test @@ -0,0 +1,297 @@ +# 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 commitstatus + +ifcapable !concurrent { + finish_test + return +} + +proc commit_status_frames {db} { + list [sqlite3_commit_status $db main FIRSTFRAME] \ + [sqlite3_commit_status $db main NFRAME] +} + +proc commit_status_conflict {db} { + list [sqlite3_commit_status $db main CONFLICT_DB] \ + [sqlite3_commit_status $db main CONFLICT_FRAME] \ + [sqlite3_commit_status $db main CONFLICT_PGNO] +} + + +do_execsql_test 1.0 { + PRAGMA journal_mode = wal; +} {wal} + +do_execsql_test 1.1 { + CREATE TABLE t1(k INTEGER PRIMARY KEY, v); +} + +do_test 1.2 { commit_status_frames db } {1 2} + +do_execsql_test 1.3 { + INSERT INTO t1 VALUES(2, 'two'); +} +do_test 1.4 { commit_status_frames db } {3 1} + +do_execsql_test 1.5 { + CREATE INDEX i1 ON t1(v); +} +do_test 1.6 { commit_status_frames db } {4 2} + +do_execsql_test 1.7 { + DROP INDEX i1; +} +do_test 1.8 { commit_status_frames db } {6 2} + +do_execsql_test 1.9 { + PRAGMA wal_checkpoint; + INSERT INTO t1 VALUES(3, 'three'); +} {0 7 7} +do_test 1.10 { commit_status_frames db } {1 1} + +do_execsql_test 1.11 { + PRAGMA journal_mode = delete; + PRAGMA journal_mode = wal2; + PRAGMA journal_size_limit = 10000; +} {delete wal2 10000} + +for {set ii 1} {$ii < 30} {incr ii} { + do_execsql_test 1.13.$ii.1 { + INSERT INTO t1(v) VALUES('value'); + } + + if {$ii<=10} { + set expect [list $ii 1] + } elseif {$ii <= 20} { + set expect [list [expr {(1+($ii-1)%10)|0x80000000}] 1] + } else { + set expect [list [expr ($ii%10)] 1] + } + + do_test 1.13.$ii.2 { commit_status_frames db } $expect + + if {$ii==15} { + execsql { PRAGMA wal_checkpoint } + } +} + +#------------------------------------------------------------------------- +reset_db + +do_execsql_test 2.0 { + CREATE TABLE t1(a, b); + CREATE TABLE t2(a, b); + PRAGMA journal_mode = wal; +} {wal} + +sqlite3 db2 test.db + +do_execsql_test -db db2 2.1 { + BEGIN CONCURRENT; + INSERT INTO t1 VALUES(1, 1); +} + +do_execsql_test 2.2 { + INSERT INTO t2 VALUES(1, 1); + INSERT INTO t2 VALUES(2, 2); + INSERT INTO t2 VALUES(3, 3); + INSERT INTO t2 VALUES(4, 4); +} + +do_execsql_test -db db2 2.3 { + COMMIT +} +do_test 2.4 { commit_status_frames db2 } {5 1} + +do_execsql_test 2.5 { + BEGIN CONCURRENT; + INSERT INTO t1 VALUES('yes', 'no'); + INSERT INTO t2 VALUES('yes', 'no'); + COMMIT; +} +do_test 2.6 { commit_status_frames db } {6 2} + +db2 close +do_execsql_test 2.7 { + PRAGMA journal_mode = delete; + PRAGMA journal_mode = wal2; +} {delete wal2} +sqlite3 db2 test.db + +do_execsql_test 2.8 { + PRAGMA journal_size_limit = 5000; + BEGIN CONCURRENT; + INSERT INTO t1 VALUES('x', 'y'); +} {5000} + +do_execsql_test -db db2 2.9 { + PRAGMA journal_size_limit = 5000; + INSERT INTO t2 VALUES(1, 1); + INSERT INTO t2 VALUES(2, 2); + INSERT INTO t2 VALUES(3, 3); + INSERT INTO t2 VALUES(4, 4); + INSERT INTO t2 VALUES(5, 5); + INSERT INTO t2 VALUES(6, 6); + INSERT INTO t2 VALUES(7, 7); + INSERT INTO t2 VALUES(8, 8); +} {5000} + +do_execsql_test 2.10 { + COMMIT; +} +do_test 2.11 { commit_status_frames db } [list [expr {0x80000000 | 4}] 1] + +do_execsql_test 2.12 { + PRAGMA wal_checkpoint; + BEGIN CONCURRENT; + INSERT INTO t1 VALUES('a', 'b'); +} {0 9 5} + +do_execsql_test -db db2 2.13 { + INSERT INTO t2 VALUES(1, 1); + INSERT INTO t2 VALUES(2, 2); + INSERT INTO t2 VALUES(3, 3); + INSERT INTO t2 VALUES(4, 4); + INSERT INTO t2 VALUES(5, 5); + INSERT INTO t2 VALUES(6, 6); + INSERT INTO t2 VALUES(7, 7); + INSERT INTO t2 VALUES(8, 8); +} {} + +do_execsql_test 2.14 { + COMMIT; +} +do_test 2.15 { commit_status_frames db } [list 8 1] + +#------------------------------------------------------------------------- +reset_db +do_execsql_test 3.0 { + CREATE TABLE t1(x); + CREATE TABLE t2(y); + CREATE TABLE t3(z); + PRAGMA journal_mode = wal2; + PRAGMA journal_size_limit = 8000; +} {wal2 8000} + +sqlite3 db2 test.db +do_execsql_test -db db2 3.1 { + BEGIN CONCURRENT; + INSERT INTO t1 VALUES('x'); +} + +do_execsql_test 3.2.0 { + INSERT INTO t2 VALUES('y'); -- frame 1 + INSERT INTO t3 VALUES('y'); -- frame 2 + INSERT INTO t2 VALUES('z'); -- frame 3 + INSERT INTO t3 VALUES('z'); -- frame 4 + INSERT INTO t1 VALUES('a'); -- frame 5 +} +do_test 3.2.1 { commit_status_frames db } {5 1} +do_execsql_test 3.2.2 { + INSERT INTO t3 VALUES('a'); -- frame 6 +} + +do_test 3.3 { + catchsql { COMMIT } db2 +} {1 {database is locked}} +execsql ROLLBACK db2 +do_test 3.4 { + commit_status_conflict db2 +} {0 5 2} + +do_execsql_test -db db2 3.5 { + BEGIN CONCURRENT; + INSERT INTO t3 VALUES('x'); +} + +do_execsql_test 3.6.0 { + INSERT INTO t2 VALUES('y'); -- frame 7 + INSERT INTO t1 VALUES('y'); -- frame 8 + INSERT INTO t2 VALUES('z'); -- frame 1b + INSERT INTO t1 VALUES('z'); -- frame 2b + INSERT INTO t3 VALUES('a'); -- frame 3b +} +do_test 3.6.1 { commit_status_frames db } [list [expr {0x80000000 | 3}] 1] +do_execsql_test 3.6.2 { + INSERT INTO t1 VALUES('a'); -- frame 4b +} + +do_test 3.7 { + catchsql { COMMIT } db2 +} {1 {database is locked}} +execsql ROLLBACK db2 +do_test 3.8 { + commit_status_conflict db2 +} [list 0 [expr {0x80000000 | 3}] 4] + +#------------------------------------------------------------------------- +reset_db +do_execsql_test 4.0 { + CREATE TABLE t1(x); + CREATE TABLE t2(y); + + PRAGMA journal_mode = wal2; + PRAGMA journal_size_limit = 10000000; +} {wal2 10000000} + +sqlite3 db2 test.db +db2 eval {SELECT * FROM sqlite_schema} + +foreach {tn s1 s2} { + 1 100 100 + 2 1000 1000 + 3 1000 1000 + 4 1000 1000 + 5 1000 1000 + 6 1000 1000 + 7 1000 1000 + 8 1000 1000 + 9 1000 1000 + + 10 2000 2000 + 11 2000 2000 + 12 2000 2000 + 13 2000 2000 + 14 2000 2000 + 15 2000 2000 + 16 2000 2000 + 17 2000 2000 +} { + execsql { + BEGIN CONCURRENT; + INSERT INTO t1 VALUES(NULL); + } db2 + + for {set ii 0} {$ii<$s1} {incr ii} { + execsql { INSERT INTO t2 VALUES(randomblob(30)); } + } + + execsql { INSERT INTO t1 VALUES(NULL) } + set frame [sqlite3_commit_status db main FIRSTFRAME] + + for {set ii 0} {$ii<$s2} {incr ii} { + execsql { INSERT INTO t2 VALUES(randomblob(30)); } + } + + do_test 4.$tn.1 { catchsql "COMMIT" db2 } {1 {database is locked}} + do_test 4.$tn.2 { + commit_status_conflict db2 + } [list 0 $frame 2] + execsql { ROLLBACK } db2 +} + +finish_test