diff --git a/ext/session/sessionstat1.test b/ext/session/sessionstat1.test index 59de1cf1b4..082c02bcf1 100644 --- a/ext/session/sessionstat1.test +++ b/ext/session/sessionstat1.test @@ -121,5 +121,191 @@ do_execsql_test -db db2 2.4 { do_execsql_test -db db2 2.5 { SELECT count(*) FROM t1 } 32 +#------------------------------------------------------------------------- +db2 close +forcedelete test.db2 +reset_db +sqlite3 db2 test.db2 + +do_test 3.0 { + do_common_sql { + CREATE TABLE t1(a, b, c); + ANALYZE; + DELETE FROM sqlite_stat1; + } + execsql { + INSERT INTO t1 VALUES(1, 1, 1); + INSERT INTO t1 VALUES(2, 2, 2); + INSERT INTO t1 VALUES(3, 3, 3); + INSERT INTO t1 VALUES(4, 4, 4); + } +} {} + +do_iterator_test 3.1 {} { + ANALYZE +} { + {INSERT sqlite_stat1 0 XX. {} {t t1 b {} t 4}} +} +db null null +db2 null null +do_execsql_test 3.2 { + SELECT * FROM sqlite_stat1; +} {t1 null 4} +do_test 3.3 { + execsql { DELETE FROM sqlite_stat1 } + do_then_apply_sql { ANALYZE } + execsql { SELECT * FROM sqlite_stat1 } db2 +} {t1 null 4} +do_test 3.4 { + execsql { INSERT INTO t1 VALUES(5,5,5) } + do_then_apply_sql { ANALYZE } + execsql { SELECT * FROM sqlite_stat1 } db2 +} {t1 null 5} +do_test 3.5 { + do_then_apply_sql { DROP TABLE t1 } + execsql { SELECT * FROM sqlite_stat1 } db2 +} {} + +do_test 3.6.1 { + execsql { + CREATE TABLE t1(a, b, c); + CREATE TABLE t2(x, y, z); + INSERT INTO t1 VALUES(1,1,1), (2,2,2), (3,3,3), (4,4,4), (5,5,5); + INSERT INTO t2 SELECT * FROM t1; + DELETE FROM sqlite_stat1; + } + sqlite3session S db main + S attach sqlite_stat1 + execsql { ANALYZE } +} {} +do_changeset_test 3.6.2 S { + {INSERT sqlite_stat1 0 XX. {} {t t2 b {} t 5}} + {INSERT sqlite_stat1 0 XX. {} {t t1 b {} t 5}} +} +do_changeset_invert_test 3.6.3 S { + {DELETE sqlite_stat1 0 XX. {t t2 b {} t 5} {}} + {DELETE sqlite_stat1 0 XX. {t t1 b {} t 5} {}} +} +do_test 3.6.4 { S delete } {} + +proc sql_changeset_concat {args} { + foreach sql $args { + sqlite3session S db main + S attach sqlite_stat1 + execsql $sql + set change [S changeset] + S delete + + if {[info vars ret]!=""} { + set ret [sqlite3changeset_concat $ret $change] + } else { + set ret $change + } + } + + changeset_to_list $ret +} + +proc do_scc_test {tn args} { + uplevel [list \ + do_test $tn [concat sql_changeset_concat [lrange $args 0 end-1]] \ + [list {*}[ lindex $args end ]] + ] +} + +do_execsql_test 3.7.0 { + DELETE FROM sqlite_stat1; +} +do_scc_test 3.7.1 { + ANALYZE; +} { + INSERT INTO t2 VALUES(6,6,6); + ANALYZE; +} { + {INSERT sqlite_stat1 0 XX. {} {t t1 b {} t 5}} + {INSERT sqlite_stat1 0 XX. {} {t t2 b {} t 6}} +} + +#------------------------------------------------------------------------- +catch { db2 close } +reset_db +forcedelete test.db2 +sqlite3 db2 test.db2 + +do_test 4.1.0 { + do_common_sql { + CREATE TABLE t1(a, b); + CREATE INDEX i1 ON t1(a); + CREATE INDEX i2 ON t1(b); + INSERT INTO t1 VALUES(1,1), (2,2); + ANALYZE; + } + execsql { DELETE FROM sqlite_stat1 } +} {} + +do_test 4.1.1 { + execsql { INSERT INTO t1 VALUES(3,3); } + set C [changeset_from_sql {ANALYZE}] + set ::c [list] + proc xConflict {args} { + lappend ::c $args + return "OMIT" + } + sqlite3changeset_apply db2 $C xConflict + set ::c +} [list {*}{ + {INSERT sqlite_stat1 CONFLICT {t t1 t i1 t {3 1}} {t t1 t i1 t {2 1}}} + {INSERT sqlite_stat1 CONFLICT {t t1 t i2 t {3 1}} {t t1 t i2 t {2 1}}} +}] + +do_execsql_test -db db2 4.1.2 { + SELECT * FROM sqlite_stat1 ORDER BY 1,2; +} {t1 i1 {2 1} t1 i2 {2 1}} + +do_test 4.1.3 { + proc xConflict {args} { + return "REPLACE" + } + sqlite3changeset_apply db2 $C xConflict + execsql { SELECT * FROM sqlite_stat1 ORDER BY 1,2 } db2 +} {t1 i1 {3 1} t1 i2 {3 1}} + +do_test 4.2.0 { + do_common_sql { + DROP TABLE t1; + CREATE TABLE t3(x,y); + INSERT INTO t3 VALUES('a','a'); + INSERT INTO t3 VALUES('b','b'); + ANALYZE; + } + execsql { DELETE FROM sqlite_stat1 } +} {} +do_test 4.2.1 { + execsql { INSERT INTO t3 VALUES('c','c'); } + set C [changeset_from_sql {ANALYZE}] + set ::c [list] + proc xConflict {args} { + lappend ::c $args + return "OMIT" + } + sqlite3changeset_apply db2 $C xConflict + set ::c +} [list {*}{ + {INSERT sqlite_stat1 CONFLICT {t t3 b {} t 3} {t t3 b {} t 2}} +}] + +db2 null null +do_execsql_test -db db2 4.2.2 { + SELECT * FROM sqlite_stat1 ORDER BY 1,2; +} {t3 null 2} + +do_test 4.2.3 { + proc xConflict {args} { + return "REPLACE" + } + sqlite3changeset_apply db2 $C xConflict + execsql { SELECT * FROM sqlite_stat1 ORDER BY 1,2 } db2 +} {t3 null 3} + finish_test diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c index 6ef90037de..ec1a93e08e 100644 --- a/ext/session/sqlite3session.c +++ b/ext/session/sqlite3session.c @@ -46,6 +46,7 @@ struct sqlite3_session { int rc; /* Non-zero if an error has occurred */ void *pFilterCtx; /* First argument to pass to xTableFilter */ int (*xTableFilter)(void *pCtx, const char *zTab); + sqlite3_value *pZeroBlob; /* Value containing X'' */ sqlite3_session *pNext; /* Next session object on same db. */ SessionTable *pTable; /* List of attached tables */ SessionHook hook; /* APIs to grab new and old data with */ @@ -113,6 +114,7 @@ struct SessionTable { SessionTable *pNext; char *zName; /* Local name of table */ int nCol; /* Number of columns in table zName */ + int bStat1; /* True if this is sqlite_stat1 */ const char **azCol; /* Column names */ u8 *abPK; /* Array of primary key flags */ int nEntry; /* Total number of entries in hash table */ @@ -496,6 +498,7 @@ static int sessionPreupdateHash( h = sessionHashAppendBlob(h, n, z); }else{ assert( eType==SQLITE_NULL ); + assert( pTab->bStat1==0 || i!=1 ); *pbNullPK = 1; } } @@ -1047,11 +1050,55 @@ static int sessionInitTable(sqlite3_session *pSession, SessionTable *pTab){ break; } } + if( 0==sqlite3_stricmp("sqlite_stat1", pTab->zName) ){ + pTab->bStat1 = 1; + } } } return (pSession->rc || pTab->abPK==0); } +/* +** Versions of the four methods in object SessionHook for use with the +** sqlite_stat1 table. The purpose of this is to substitute a zero-length +** blob each time a NULL value is read from the "idx" column of the +** sqlite_stat1 table. +*/ +typedef struct SessionStat1Ctx SessionStat1Ctx; +struct SessionStat1Ctx { + SessionHook hook; + sqlite3_session *pSession; +}; +static int sessionStat1Old(void *pCtx, int iCol, sqlite3_value **ppVal){ + SessionStat1Ctx *p = (SessionStat1Ctx*)pCtx; + sqlite3_value *pVal = 0; + int rc = p->hook.xOld(p->hook.pCtx, iCol, &pVal); + if( rc==SQLITE_OK && iCol==1 && sqlite3_value_type(pVal)==SQLITE_NULL ){ + pVal = p->pSession->pZeroBlob; + } + *ppVal = pVal; + return rc; +} +static int sessionStat1New(void *pCtx, int iCol, sqlite3_value **ppVal){ + SessionStat1Ctx *p = (SessionStat1Ctx*)pCtx; + sqlite3_value *pVal = 0; + int rc = p->hook.xNew(p->hook.pCtx, iCol, &pVal); + if( rc==SQLITE_OK && iCol==1 && sqlite3_value_type(pVal)==SQLITE_NULL ){ + pVal = p->pSession->pZeroBlob; + } + *ppVal = pVal; + return rc; +} +static int sessionStat1Count(void *pCtx){ + SessionStat1Ctx *p = (SessionStat1Ctx*)pCtx; + return p->hook.xCount(p->hook.pCtx); +} +static int sessionStat1Depth(void *pCtx){ + SessionStat1Ctx *p = (SessionStat1Ctx*)pCtx; + return p->hook.xDepth(p->hook.pCtx); +} + + /* ** This function is only called from with a pre-update-hook reporting a ** change on table pTab (attached to session pSession). The type of change @@ -1068,6 +1115,7 @@ static void sessionPreupdateOneChange( int iHash; int bNull = 0; int rc = SQLITE_OK; + SessionStat1Ctx stat1; if( pSession->rc ) return; @@ -1087,6 +1135,25 @@ static void sessionPreupdateOneChange( return; } + if( pTab->bStat1 ){ + stat1.hook = pSession->hook; + stat1.pSession = pSession; + pSession->hook.pCtx = (void*)&stat1; + pSession->hook.xNew = sessionStat1New; + pSession->hook.xOld = sessionStat1Old; + pSession->hook.xCount = sessionStat1Count; + pSession->hook.xDepth = sessionStat1Depth; + if( pSession->pZeroBlob==0 ){ + sqlite3_value *p = sqlite3ValueNew(0); + if( p==0 ){ + rc = SQLITE_NOMEM; + goto error_out; + } + sqlite3ValueSetStr(p, 0, "", 0, SQLITE_STATIC); + pSession->pZeroBlob = p; + } + } + /* Calculate the hash-key for this change. If the primary key of the row ** includes a NULL value, exit early. Such changes are ignored by the ** session module. */ @@ -1176,6 +1243,9 @@ static void sessionPreupdateOneChange( /* If an error has occurred, mark the session object as failed. */ error_out: + if( pTab->bStat1 ){ + pSession->hook = stat1.hook; + } if( rc!=SQLITE_OK ){ pSession->rc = rc; } @@ -1637,6 +1707,7 @@ void sqlite3session_delete(sqlite3_session *pSession){ } } sqlite3_mutex_leave(sqlite3_db_mutex(db)); + sqlite3ValueFree(pSession->pZeroBlob); /* Delete all attached table objects. And the contents of their ** associated hash-tables. */ @@ -2104,28 +2175,41 @@ static int sessionSelectStmt( sqlite3_stmt **ppStmt /* OUT: Prepared SELECT statement */ ){ int rc = SQLITE_OK; - int i; - const char *zSep = ""; - SessionBuffer buf = {0, 0, 0}; + char *zSql = 0; + int nSql = -1; - sessionAppendStr(&buf, "SELECT * FROM ", &rc); - sessionAppendIdent(&buf, zDb, &rc); - sessionAppendStr(&buf, ".", &rc); - sessionAppendIdent(&buf, zTab, &rc); - sessionAppendStr(&buf, " WHERE ", &rc); - for(i=0; ipInsert, + "INSERT INTO main.sqlite_stat1 VALUES(?1, " + "CASE WHEN length(?2)=0 AND typeof(?2)='blob' THEN NULL ELSE ?2 END, " + "?3)" + ); + } + if( rc==SQLITE_OK ){ + rc = sessionPrepare(db, &p->pUpdate, + "UPDATE main.sqlite_stat1 SET " + "tbl = CASE WHEN ?2 THEN ?3 ELSE tbl END, " + "idx = CASE WHEN ?5 THEN ?6 ELSE idx END, " + "stat = CASE WHEN ?8 THEN ?9 ELSE stat END " + "WHERE tbl=?1 AND idx IS " + "CASE WHEN length(?4)=0 AND typeof(?4)='blob' THEN NULL ELSE ?4 END " + "AND (?10 OR ?8=0 OR stat IS ?7)" + ); + } + if( rc==SQLITE_OK ){ + rc = sessionPrepare(db, &p->pDelete, + "DELETE FROM main.sqlite_stat1 WHERE tbl=?1 AND idx IS " + "CASE WHEN length(?2)=0 AND typeof(?2)='blob' THEN NULL ELSE ?2 END " + "AND (?4 OR stat IS ?3)" + ); + } + assert( rc==SQLITE_OK ); + return rc; +} + /* ** A wrapper around sqlite3_bind_value() that detects an extra problem. ** See comments in the body of this function for details. @@ -3855,11 +3981,25 @@ static int sessionApplyOneOp( }else{ assert( op==SQLITE_INSERT ); - rc = sessionBindRow(pIter, sqlite3changeset_new, nCol, 0, p->pInsert); - if( rc!=SQLITE_OK ) return rc; + if( p->bStat1 ){ + /* Check if there is a conflicting row. For sqlite_stat1, this needs + ** to be done using a SELECT, as there is no PRIMARY KEY in the + ** database schema to throw an exception if a duplicate is inserted. */ + rc = sessionSeekToRow(p->db, pIter, p->abPK, p->pSelect); + if( rc==SQLITE_ROW ){ + rc = SQLITE_CONSTRAINT; + sqlite3_reset(p->pSelect); + } + } + + if( rc==SQLITE_OK ){ + rc = sessionBindRow(pIter, sqlite3changeset_new, nCol, 0, p->pInsert); + if( rc!=SQLITE_OK ) return rc; + + sqlite3_step(p->pInsert); + rc = sqlite3_reset(p->pInsert); + } - sqlite3_step(p->pInsert); - rc = sqlite3_reset(p->pInsert); if( (rc&0xff)==SQLITE_CONSTRAINT ){ rc = sessionConflictHandler( SQLITE_CHANGESET_CONFLICT, p, pIter, xConflict, pCtx, pbReplace @@ -4092,12 +4232,20 @@ static int sessionChangesetApply( } else{ sApply.nCol = nCol; - if((rc = sessionSelectRow(db, zTab, &sApply)) - || (rc = sessionUpdateRow(db, zTab, &sApply)) - || (rc = sessionDeleteRow(db, zTab, &sApply)) - || (rc = sessionInsertRow(db, zTab, &sApply)) - ){ - break; + if( 0==sqlite3_stricmp(zTab, "sqlite_stat1") ){ + if( (rc = sessionStat1Sql(db, &sApply) ) ){ + break; + } + sApply.bStat1 = 1; + }else{ + if((rc = sessionSelectRow(db, zTab, &sApply)) + || (rc = sessionUpdateRow(db, zTab, &sApply)) + || (rc = sessionDeleteRow(db, zTab, &sApply)) + || (rc = sessionInsertRow(db, zTab, &sApply)) + ){ + break; + } + sApply.bStat1 = 0; } } nTab = sqlite3Strlen30(zTab); diff --git a/ext/session/sqlite3session.h b/ext/session/sqlite3session.h index b22df129cb..9f33855df0 100644 --- a/ext/session/sqlite3session.h +++ b/ext/session/sqlite3session.h @@ -147,6 +147,35 @@ int sqlite3session_indirect(sqlite3_session *pSession, int bIndirect); ** ** SQLITE_OK is returned if the call completes without error. Or, if an error ** occurs, an SQLite error code (e.g. SQLITE_NOMEM) is returned. +** +**

Special sqlite_stat1 Handling

+** +** As of SQLite version 3.22.0, the "sqlite_stat1" table is an exception to +** some of the rules above. In SQLite, the schema of sqlite_stat1 is: +**
+**        CREATE TABLE sqlite_stat1(tbl,idx,stat)  
+**  
+** +** Even though sqlite_stat1 does not have a PRIMARY KEY, changes are +** recorded for it as if the PRIMARY KEY is (tbl,idx). Additionally, changes +** are recorded for rows for which (idx IS NULL) is true. However, for such +** rows a zero-length blob (SQL value X'') is stored in the changeset or +** patchset instead of a NULL value. This allows such changesets to be +** manipulated by legacy implementations of sqlite3changeset_invert(), +** concat() and similar. +** +** The sqlite3changeset_apply() function automatically converts the +** zero-length blob back to a NULL value when updating the sqlite_stat1 +** table. However, if the application calls sqlite3changeset_new(), +** sqlite3changeset_old() or sqlite3changeset_conflict on a changeset +** iterator directly (including on a changeset iterator passed to a +** conflict-handler callback) then the X'' value is returned. The application +** must translate X'' to NULL itself if required. +** +** Legacy (older than 3.22.0) versions of the sessions module cannot capture +** changes made to the sqlite_stat1 table. Legacy versions of the +** sqlite3changeset_apply() function silently ignore any modifications to the +** sqlite_stat1 table that are part of a changeset or patchset. */ int sqlite3session_attach( sqlite3_session *pSession, /* Session object */ diff --git a/manifest b/manifest index d77626ecd2..3cd31bd5cc 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Fix\sto\sthe\sdocumentation\sfor\ssqlite3_trace_v2().\s\sNo\schanges\sto\scode. -D 2018-01-18T16:52:35.238 +C Fix\ssessions\smodule\shandling\sof\ssqlite_stat1\srows\swith\s(idx\sIS\sNULL). +D 2018-01-18T16:59:52.652 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F Makefile.in 38f84f301cbef443b2d269f67a74b8cc536469831f70df7c3e912acc04932cc2 @@ -398,10 +398,10 @@ F ext/session/sessionat.test efe88965e74ff1bc2af9c310b28358c02d420c1fb2705cc7a28 F ext/session/sessiondiff.test ad13dd65664bae26744e1f18eb3cbd5588349b7e9118851d8f9364248d67bcec F ext/session/sessionfault.test da273f2712b6411e85e71465a1733b8501dbf6f7 F ext/session/sessionfault2.test 04aa0bc9aa70ea43d8de82c4f648db4de1e990b0 -F ext/session/sessionstat1.test e3a3f5876ce1526b48f6f447ee0b18960ac683e3fc891791e1ca0c08e823d498 +F ext/session/sessionstat1.test 41cd97c2e48619a41cdf8ae749e1b25f34719de638689221aa43971be693bf4e F ext/session/sessionwor.test 2f3744236dc8b170a695b7d8ddc8c743c7e79fdc -F ext/session/sqlite3session.c c9d813ffa8db0670257da0a0c931e7188dfbb0010bf1a38274775c9b6300dd5c -F ext/session/sqlite3session.h cb4d860101ba6d3ac810f18684539b766d24d668fa2436cdde90d711af9464fb +F ext/session/sqlite3session.c 989466bba4dff0ede8d4c450b1fc65ca222b87e31193eddbf3931b88bf898a57 +F ext/session/sqlite3session.h 01774161cbd328fe3d496323655b9cc142317ff1fb1ae15c1232075ea240e3a4 F ext/session/test_session.c eb0bd6c1ea791c1d66ee4ef94c16500dad936386 F ext/userauth/sqlite3userauth.h 7f3ea8c4686db8e40b0a0e7a8e0b00fac13aa7a3 F ext/userauth/user-auth.txt e6641021a9210364665fe625d067617d03f27b04 @@ -422,7 +422,7 @@ F sqlite.pc.in 42b7bf0d02e08b9e77734a47798d1a55a9e0716b F sqlite3.1 fc7ad8990fc8409983309bb80de8c811a7506786 F sqlite3.pc.in 48fed132e7cb71ab676105d2a4dc77127d8c1f3a F src/alter.c cf7a8af45cb0ace672f47a1b29ab24092a9e8cd8d945a9974e3b5d925f548594 -F src/analyze.c f9bfffd0416c547a916cb96793b94684bdb0d26a71542ea31819c6757741c19d +F src/analyze.c 6b42e36a5dcc2703a771f2411bd5e99524bd62c7ecde209bb88dfb04c72f046e F src/attach.c 84c477e856b24c2b9a0983b438a707c0cf4d616cee7a425401d418e58afec24c F src/auth.c 6277d63837357549fe14e723490d6dc1a38768d71c795c5eb5c0f8a99f918f73 F src/backup.c faf17e60b43233c214aae6a8179d24503a61e83b @@ -1700,7 +1700,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 469b96be5350ba2291518280ffe179b87aa7fbe701e2813ef63843922771517a -R 2b93eecc1bfa73dda9a58b538de6c0af -U drh -Z f64363038b04686cc1ed3a7aa13703ce +P 6fbd0a11a66f8eb4d7820cb49c23bdcb917db98a22c29d76edea1eea6dab0a4e dc7c48cb4126db9e25c73512cc743155293fe1c4c2516f8c84102228695b6e70 +R e340b654f2343cebf57c71c1eef74e4f +T +closed dc7c48cb4126db9e25c73512cc743155293fe1c4c2516f8c84102228695b6e70 +U dan +Z 94f64c633d9eca20229572c5d0ac1d82 diff --git a/manifest.uuid b/manifest.uuid index 9ea54531fe..368a3978bc 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -6fbd0a11a66f8eb4d7820cb49c23bdcb917db98a22c29d76edea1eea6dab0a4e \ No newline at end of file +0e916416331d7948b312a5dd58ac0c145030bb3b47a37dab2636564397249a86 \ No newline at end of file diff --git a/src/analyze.c b/src/analyze.c index 85c603ffdf..0d13d77790 100644 --- a/src/analyze.c +++ b/src/analyze.c @@ -1309,6 +1309,9 @@ static void analyzeOneTable( sqlite3VdbeAddOp2(v, OP_NewRowid, iStatCur, regNewRowid); sqlite3VdbeAddOp3(v, OP_Insert, iStatCur, regTemp, regNewRowid); sqlite3VdbeChangeP5(v, OPFLAG_APPEND); +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + sqlite3VdbeChangeP4(v, -1, (char*)pStat1, P4_TABLE); +#endif sqlite3VdbeJumpHere(v, jZeroRows); } }