From 4dd11e33e4903eca0abb7a0539accd23bb72b502 Mon Sep 17 00:00:00 2001 From: dan Date: Mon, 18 May 2026 18:59:06 +0000 Subject: [PATCH 1/5] Update the session module so that it can apply changesets containing two or more UPDATE changes that form a dependency loop - so that no single UPDATE can be applied independently without violating a constraint. FossilOrigin-Name: 5f8010599fb33fbe1dadc894a679056a5d38040ab4f09772cd59827941e9348b --- ext/session/sessionG.test | 5 +- ext/session/sessionconflict2.test | 68 ++++++ ext/session/sqlite3session.c | 384 +++++++++++++++++++++++++++--- manifest | 20 +- manifest.tags | 4 +- manifest.uuid | 2 +- 6 files changed, 440 insertions(+), 43 deletions(-) create mode 100755 ext/session/sessionconflict2.test diff --git a/ext/session/sessionG.test b/ext/session/sessionG.test index 1ebcc926a5..58713a5b62 100644 --- a/ext/session/sessionG.test +++ b/ext/session/sessionG.test @@ -82,6 +82,9 @@ do_test 2.2.1 { # It is not possible to apply the changeset generated by the following # SQL, as none of the three updated rows may be updated as part of the # first pass. + # + # UPDATE 19/05/2026 - it is now possible to apply such an update. + # do_then_apply_sql -ignorenoop { UPDATE t1 SET b=0 WHERE a=1; UPDATE t1 SET b=1 WHERE a=2; @@ -89,7 +92,7 @@ do_test 2.2.1 { UPDATE t1 SET b=3 WHERE a=1; } db2 eval { SELECT a, b FROM t1 } -} {1 1 2 2 3 3} +} {1 3 2 1 3 2} do_test 2.2.2 { db eval { SELECT a, b FROM t1 } } {1 3 2 1 3 2} #------------------------------------------------------------------------- diff --git a/ext/session/sessionconflict2.test b/ext/session/sessionconflict2.test new file mode 100755 index 0000000000..d6fcde73ab --- /dev/null +++ b/ext/session/sessionconflict2.test @@ -0,0 +1,68 @@ +# 2026 May 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 implements regression tests for SQLite library. +# + +if {![info exists testdir]} { + set testdir [file join [file dirname [info script]] .. .. test] +} +source [file join [file dirname [info script]] session_common.tcl] +source $testdir/tester.tcl +ifcapable !session {finish_test; return} + +set testprefix sessionconflict2 + +#db close +#sqlite3_shutdown +#test_sqlite3_log log +#proc log {code msg} { puts "LOG $code $msg" } +#sqlite3 db test.db + +forcedelete test.db2 +sqlite3 db2 test.db2 + +do_test 1.0 { + do_common_sql { + CREATE TABLE t1(a PRIMARY KEY, b, c UNIQUE); + INSERT INTO t1 VALUES(1, 1, 1); + INSERT INTO t1 VALUES(2, 2, 2); + INSERT INTO t1 VALUES(3, 3, 3); + } +} {} + +do_test 1.1 { + do_then_apply_sql { + UPDATE t1 SET c=NULL WHERE a=1; + UPDATE t1 SET c=1 WHERE a=3; + UPDATE t1 SET c=3 WHERE a=1; + } +} {} + +do_execsql_test -db db 1.2 { + SELECT rowid, * FROM t1 +} { + 1 1 1 3 + 2 2 2 2 + 3 3 3 1 +} + +do_execsql_test -db db2 1.3 { + SELECT rowid, * FROM t1 +} { + 1 1 1 3 + 2 2 2 2 + 3 3 3 1 +} + + + +db2 close +finish_test diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c index 63aeb58fcf..e8fc626a3e 100644 --- a/ext/session/sqlite3session.c +++ b/ext/session/sqlite3session.c @@ -1545,6 +1545,16 @@ static int sessionPrepareDfltStmt( return rc; } +/* +** Finalize statement pStmt. If (*pRc) is SQLITE_OK when this function is +** called, set it to the results of the sqlite3_finalize() call. Or, if +** it is already set to an error code, leave it as is. +*/ +static void sessionFinalizeStmt(sqlite3_stmt *pStmt, int *pRc){ + int rc = sqlite3_finalize(pStmt); + if( *pRc==SQLITE_OK ) *pRc = rc; +} + /* ** Table pTab has one or more existing change-records with old.* records ** with fewer than pTab->nCol columns. This function updates all such @@ -1567,9 +1577,8 @@ static int sessionUpdateChanges(sqlite3_session *pSession, SessionTable *pTab){ } } + sessionFinalizeStmt(pStmt, &rc); pSession->rc = rc; - rc = sqlite3_finalize(pStmt); - if( pSession->rc==SQLITE_OK ) pSession->rc = rc; return pSession->rc; } @@ -2895,11 +2904,11 @@ static int sessionSelectStmt( ); sessionAppendStr(&cols, "tbl, ?2, stat", &rc); }else{ - #if 0 +#if 0 if( bRowid ){ sessionAppendStr(&cols, SESSIONS_ROWID, &rc); } - #endif +#endif for(i=0; iin.aData[pIter->in.iCurrent]; int nBlob = pIter->in.iNext - pIter->in.iCurrent; sessionAppendBlob(&p->constraints, aBlob, nBlob, &rc); - return SQLITE_OK; + return rc; }else if( p->bIgnoreNoop==0 || op!=SQLITE_DELETE || eType==SQLITE_CHANGESET_CONFLICT ){ @@ -5170,7 +5179,263 @@ static int sessionApplyOneWithRetry( } /* -** Retry the changes accumulated in the pApply->constraints buffer. +** Create an iterator to iterate through the retry buffer pRetry. +*/ +static int sessionRetryIterInit( + SessionBuffer *pRetry, /* Buffer to iterate through */ + int bPatchset, /* True for patchset, false for changeset */ + const char *zTab, /* Table name */ + SessionApplyCtx *pApply, /* Session apply context */ + sqlite3_changeset_iter **ppIter /* OUT: New iterator */ +){ + sqlite3_changeset_iter *pRet = 0; + int rc = SQLITE_OK; + + rc = sessionChangesetStart( + &pRet, 0, 0, pRetry->nBuf, pRetry->aBuf, pApply->bInvertConstraints, 1 + ); + if( rc==SQLITE_OK ){ + size_t nByte = 2*pApply->nCol*sizeof(sqlite3_value*); + pRet->bPatchset = bPatchset; + pRet->zTab = (char*)zTab; + pRet->nCol = pApply->nCol; + pRet->abPK = pApply->abPK; + sessionBufferGrow(&pRet->tblhdr, nByte, &rc); + pRet->apValue = (sqlite3_value**)pRet->tblhdr.aBuf; + if( rc==SQLITE_OK ) memset(pRet->apValue, 0, nByte); + } + + *ppIter = pRet; + return rc; +} + +/* +** Attempt to apply all the changes in retry buffer pRetry to the database. +** Except, if parameter iSkip is greater than or equal to 0, skip change +** iSkip. +*/ +static int sessionApplyRetryBuffer( + SessionBuffer *pRetry, /* Buffer to apply changes from */ + int iSkip, /* If >=0, index of change to omit */ + sqlite3 *db, /* Database handle */ + int bPatchset, /* True for patchset, false for changeset */ + const char *zTab, /* Name of table to write to */ + SessionApplyCtx *pApply, /* Apply context */ + int(*xConflict)(void*, int, sqlite3_changeset_iter*), + void *pCtx /* First argument passed to xConflict */ +){ + int rc = SQLITE_OK; + int rc2 = SQLITE_OK; + int ii = 0; + sqlite3_changeset_iter *pIter = 0; + + assert( pApply->constraints.nBuf==0 ); + + rc = sessionRetryIterInit(pRetry, bPatchset, zTab, pApply, &pIter); + + for(ii=0; rc==SQLITE_OK && SQLITE_ROW==sqlite3changeset_next(pIter); ii++){ + if( ii!=iSkip ){ + rc = sessionApplyOneWithRetry(db, pIter, pApply, xConflict, pCtx); + } + } + + rc2 = sqlite3changeset_finalize(pIter); + if( rc==SQLITE_OK ) rc = rc2; + assert( pApply->bDeferConstraints || pApply->constraints.nBuf==0 ); + + return rc; +} + +/* +** Check if table zTab in the "main" database of db is a WITHOUT ROWID +** table. +** +** If no error occurs, return SQLITE_OK and set output variable (*pbWR) to +** true if zTab is a WITHOUT ROWID table, or false otherwise. Or, if an +** error does occur, return an SQLite error code. The final value of (*pbWR) +** is undefined in this case. +*/ +static int sessionTableIsWithoutRowid(sqlite3 *db, const char *zTab, int *pbWR){ + sqlite3_stmt *pList = 0; + char *zSql = 0; + int rc = SQLITE_OK; + + zSql = sqlite3_mprintf("PRAGMA table_list = %Q", zTab); + if( zSql==0 ){ + rc = SQLITE_NOMEM; + }else{ + rc = sqlite3_prepare_v2(db, zSql, -1, &pList, 0); + sqlite3_free(zSql); + } + + if( rc==SQLITE_OK ){ + if( SQLITE_ROW==sqlite3_step(pList) ){ + *pbWR = sqlite3_column_int(pList, 4); + } + rc = sqlite3_finalize(pList); + } + + return rc; +} + +/* +** Iterator pUp points to an UPDATE change. This function deletes the +** affected row from the database and creates an INSERT statement that +** may be used to reinsert the row as it is after the UPDATE change +** has been applied. +** +** If successful, SQLITE_OK is returned and output variable (*ppInsert) +** is left pointing to a prepared INSERT statement. It is the responsibility +** of the caller to eventually free this statement using sqlite3_finalize(). +** Or, if an error occurs, an SQLite error code is returned and (*ppInsert) +** set to NULL. pApply->zErr may be set to an error message in this case. +*/ +static int sessionUpdateToDeleteInsert( + sqlite3 *db, /* Database to write to */ + const char *zTab, /* Table name */ + SessionApplyCtx *pApply, /* Apply context */ + sqlite3_changeset_iter *pUp, /* Iterator pointing to UPDATE change */ + sqlite3_stmt **ppInsert /* OUT: INSERT statement */ +){ + sqlite3_stmt *pRet = 0; /* The INSERT statement */ + sqlite3_stmt *pSelect = 0; /* SELECT to read current values of row */ + int rc = SQLITE_OK; + int bWR = 0; + + rc = sessionTableIsWithoutRowid(db, zTab, &bWR); + if( rc==SQLITE_OK ){ + char *zSelect = 0; + char *zInsert = 0; + SessionBuffer cols = {0, 0, 0}; + SessionBuffer insbind = {0, 0, 0}; + SessionBuffer pkcols = {0, 0, 0}; + SessionBuffer selbind = {0, 0, 0}; + + const char *zComma = ""; + const char *zComma2 = ""; + int ii; + for(ii=0; iinCol; ii++){ + sessionAppendStr(&cols, zComma, &rc); + sessionAppendIdent(&cols, pApply->azCol[ii], &rc); + sessionAppendStr(&insbind, zComma, &rc); + sessionAppendStr(&insbind, "?", &rc); + zComma = ", "; + + if( pApply->abPK[ii] ){ + sessionAppendStr(&pkcols, zComma2, &rc); + sessionAppendIdent(&pkcols, pApply->azCol[ii], &rc); + sessionAppendStr(&selbind, zComma2, &rc); + sessionAppendPrintf(&selbind, &rc, "?%d", ii+1); + zComma2 = ", "; + } + } + if( bWR==0 ){ + sessionAppendStr(&cols, zComma, &rc); + sessionAppendStr(&cols, SESSIONS_ROWID, &rc); + sessionAppendStr(&insbind, zComma, &rc); + sessionAppendStr(&insbind, "?", &rc); + } + + if( rc==SQLITE_OK ){ + zSelect = sqlite3_mprintf("SELECT %s FROM %Q WHERE (%s) IS (%s)", + cols.aBuf, zTab, pkcols.aBuf, selbind.aBuf + ); + if( zSelect==0 ) rc = SQLITE_NOMEM; + } + if( rc==SQLITE_OK ){ + zInsert = sqlite3_mprintf("INSERT INTO %Q(%s) VALUES(%s)", + zTab, cols.aBuf, insbind.aBuf + ); + if( zInsert==0 ) rc = SQLITE_NOMEM; + } + + if( rc==SQLITE_OK ){ + rc = sessionPrepare(db, &pSelect, &pApply->zErr, zSelect); + } + if( rc==SQLITE_OK ){ + rc = sessionPrepare(db, &pRet, &pApply->zErr, zInsert); + } + + sqlite3_free(zSelect); + sqlite3_free(zInsert); + sqlite3_free(cols.aBuf); + sqlite3_free(insbind.aBuf); + sqlite3_free(pkcols.aBuf); + sqlite3_free(selbind.aBuf); + } + + if( rc==SQLITE_OK ){ + rc = sessionBindRow( + pUp, sqlite3changeset_old, pApply->nCol, pApply->abPK, pSelect + ); + } + if( rc==SQLITE_OK && sqlite3_step(pSelect)!=SQLITE_ROW ){ + rc = SQLITE_ERROR; + } + + if( rc==SQLITE_OK ){ + int iCol; + for(iCol=0; iColnCol; iCol++){ + sqlite3_value *pVal = pUp->apValue[iCol+pApply->nCol]; + if( pVal==0 ){ + pVal = sqlite3_column_value(pSelect, iCol); + } + rc = sqlite3_bind_value(pRet, iCol+1, pVal); + } + if( bWR==0 ){ + sqlite3_bind_int64(pRet, iCol+1, sqlite3_column_int64(pSelect, iCol)); + } + } + sessionFinalizeStmt(pSelect, &rc); + + /* Delete the row from the database. */ + if( rc==SQLITE_OK ){ + rc = sessionBindRow( + pUp, sqlite3changeset_old, pApply->nCol, pApply->abPK, pApply->pDelete + ); + sqlite3_bind_int(pApply->pDelete, pApply->nCol+1, 1); + } + if( rc==SQLITE_OK ){ + sqlite3_step(pApply->pDelete); + rc = sqlite3_reset(pApply->pDelete); + } + + if( rc!=SQLITE_OK ){ + sqlite3_finalize(pRet); + pRet = 0; + } + + *ppInsert = pRet; + return rc; +} + +/* +** Retry the changes accumulated in the pApply->constraints buffer. The +** pApply->constraints buffer contains all changes to table zTab that +** could not be applied due to SQLITE_CONSTRAINT errors. This function +** attempts to apply them as follows: +** +** 1) It runs through the buffer and attempts to retry each change, +** removing any that are successfully applied from the buffer. This +** is repeated until no further progress can be made. +** +** 2) For each UPDATE change in the buffer, try the following in a +** savepoint transaction: +** +** a) DELETE the affected row, +** b) Attempt step (1) with remaining changes, +** c) Attempt to INSERT a row equivalent to the one that would be +** created by applying this UPDATE change. +** +** If the INSERT in (c) succeeds, the savepoint is committed and all +** successfully applied changes are removed from the buffer. Step (2) +** is then repeated. +** +** 3) Once step (2) has been attempted for each UPDATE in the change, +** a final attempt is made to apply each remaining change. This time, +** if an SQLITE_CONSTRAINT error is encountered, the conflict handler +** is invoked and the user has to decide whether to omit the change +** or rollback the entire _apply() operation. */ static int sessionRetryConstraints( sqlite3 *db, @@ -5181,41 +5446,98 @@ static int sessionRetryConstraints( void *pCtx /* First argument passed to xConflict */ ){ int rc = SQLITE_OK; + int iUpdate = 0; + /* Step (1) */ while( pApply->constraints.nBuf ){ - sqlite3_changeset_iter *pIter2 = 0; SessionBuffer cons = pApply->constraints; memset(&pApply->constraints, 0, sizeof(SessionBuffer)); - rc = sessionChangesetStart( - &pIter2, 0, 0, cons.nBuf, cons.aBuf, pApply->bInvertConstraints, 1 + rc = sessionApplyRetryBuffer( + &cons, -1, db, bPatchset, zTab, pApply, xConflict, pCtx ); - if( rc==SQLITE_OK ){ - size_t nByte = 2*pApply->nCol*sizeof(sqlite3_value*); - int rc2; - pIter2->bPatchset = bPatchset; - pIter2->zTab = (char*)zTab; - pIter2->nCol = pApply->nCol; - pIter2->abPK = pApply->abPK; - sessionBufferGrow(&pIter2->tblhdr, nByte, &rc); - pIter2->apValue = (sqlite3_value**)pIter2->tblhdr.aBuf; - if( rc==SQLITE_OK ) memset(pIter2->apValue, 0, nByte); - - while( rc==SQLITE_OK && SQLITE_ROW==sqlite3changeset_next(pIter2) ){ - rc = sessionApplyOneWithRetry(db, pIter2, pApply, xConflict, pCtx); - } - - rc2 = sqlite3changeset_finalize(pIter2); - if( rc==SQLITE_OK ) rc = rc2; - } - assert( pApply->bDeferConstraints || pApply->constraints.nBuf==0 ); sqlite3_free(cons.aBuf); if( rc!=SQLITE_OK ) break; - if( pApply->constraints.nBuf>=cons.nBuf ){ - /* No progress was made on the last round. */ - pApply->bDeferConstraints = 0; + + /* If no progress has been made this round, break out of the loop. */ + if( pApply->constraints.nBuf>=cons.nBuf ) break; + } + + /* Step (2) */ + while( rc==SQLITE_OK && pApply->constraints.nBuf ){ + SessionBuffer cons = {0, 0, 0}; + sqlite3_changeset_iter *pUp = 0; + sqlite3_stmt *pInsert = 0; + int iSkip = 0; + + rc = sessionRetryIterInit( + &pApply->constraints, bPatchset, zTab, pApply, &pUp + ); + if( rc==SQLITE_OK ){ + int iThis = -1; + while( SQLITE_ROW==sqlite3changeset_next(pUp) ){ + if( pUp->op==SQLITE_UPDATE ) iThis++; + if( iThis==iUpdate ) break; + iSkip++; + } + if( iThis==iUpdate ){ + rc = sqlite3_exec(db, "SAVEPOINT update_op", 0, 0, 0); + if( rc==SQLITE_OK ){ + rc = sessionUpdateToDeleteInsert(db, zTab, pApply, pUp, &pInsert); + } + } + sqlite3changeset_finalize(pUp); + if( iThis!=iUpdate ) break; } + + if( rc==SQLITE_OK ){ + cons = pApply->constraints; + + while( rc==SQLITE_OK && pApply->constraints.nBuf>0 ){ + SessionBuffer app = pApply->constraints; + memset(&pApply->constraints, 0, sizeof(SessionBuffer)); + rc = sessionApplyRetryBuffer( + &app, iSkip, db, bPatchset, zTab, pApply, xConflict, pCtx + ); + if( app.aBuf!=cons.aBuf ){ + sqlite3_free(app.aBuf); + } + if( pApply->constraints.nBuf>=app.nBuf ){ + break; + } + } + } + + iUpdate++; + if( rc==SQLITE_OK ){ + sqlite3_step(pInsert); + rc = sqlite3_finalize(pInsert); + if( rc==SQLITE_CONSTRAINT ){ + rc = sqlite3_exec(db, "ROLLBACK TO update_op", 0, 0, 0); + sqlite3_free(pApply->constraints.aBuf); + pApply->constraints = cons; + memset(&cons, 0, sizeof(cons)); + }else if( rc==SQLITE_OK ){ + iUpdate = 0; + } + if( rc==SQLITE_OK ){ + rc = sqlite3_exec(db, "RELEASE update_op", 0, 0, 0); + } + } + + sqlite3_free(cons.aBuf); + } + + /* Step (3) */ + if( rc==SQLITE_OK && pApply->constraints.nBuf ){ + SessionBuffer cons = pApply->constraints; + memset(&pApply->constraints, 0, sizeof(SessionBuffer)); + pApply->bDeferConstraints = 0; + rc = sessionApplyRetryBuffer( + &cons, -1, db, bPatchset, zTab, pApply, xConflict, pCtx + ); + sqlite3_free(cons.aBuf); } return rc; diff --git a/manifest b/manifest index 7a6119e7f2..8bf4b6321e 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Fix\sthe\swindow-function\svariant\sof\sthe\sjson_group_object()\sfunction\sso\nthat\sit\scorrectly\shandles\sNULL\sentries.\n[bugs:/forumpost/0de87b23b3|Bug\sreport\s0de87b23b3]. -D 2026-05-18T17:55:40.195 +C Update\sthe\ssession\smodule\sso\sthat\sit\scan\sapply\schangesets\scontaining\stwo\sor\smore\sUPDATE\schanges\sthat\sform\sa\sdependency\sloop\s-\sso\sthat\sno\ssingle\sUPDATE\scan\sbe\sapplied\sindependently\swithout\sviolating\sa\sconstraint. +D 2026-05-18T18:59:06.664 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -546,7 +546,7 @@ F ext/session/sessionC.test de98b5e173fd86c79af0d0541534398d2ea75dc0d5d74a00103e F ext/session/sessionD.test 470ff917dc849e2eb78142ade63aaabd729d773833cff0ff01bca0eda68a21ce F ext/session/sessionE.test b2010949c9d7415306f64e3c2072ddabc4b8250c98478d3c0c4d064bce83111d F ext/session/sessionF.test d37ed800881e742c208df443537bf29aa49fd56eac520d0f0c6df3e6320f3401 -F ext/session/sessionG.test 3efe388282d641b65485b5462e67851002cd91a282dc95b685d085eb8efdad0a +F ext/session/sessionG.test 64c2b69531aebdb36d5977a5f832d77e4c8bda0c746a6c630adf23660bb1c7c2 F ext/session/sessionH.test 71bbff6b1abb2c4ac62b84dee53273c37e0b21e5fde3aed80929403e091ef859 F ext/session/sessionI.test 11e7b6729fc942982a5104a40132f70a2e964d64d60dc5809b8206465af74822 F ext/session/session_common.tcl a31f537a929a695a852d241c9434f2847cadf329856401921139fbb03a5a7697 @@ -559,6 +559,7 @@ F ext/session/sessionblob.test 87faf667870b72f08e91969abd9f52a383ab7b514506ee194 F ext/session/sessionchange.test 6618cb1c1338a4b6df173b6ac42d09623fb71269962abf23ebb7617fe9f45a50 F ext/session/sessionchange2.test 8f59185216882adc8b34bb5ba63887459acf3df58493bcffa12e4d05ab6a6b85 F ext/session/sessionconflict.test 19e4a53795c4c930bfec49e809311e09b2a9e202d9446e56d7a8b139046a0c07 x +F ext/session/sessionconflict2.test 9d65dd1b23c6ddd94134260c68ac3a7513683fbcfecc4d32344a416db115efa9 x F ext/session/sessiondiff.test e89f7aedcdd89e5ebac3a455224eb553a171e9586fc3e1e6a7b3388d2648ba8d F ext/session/sessionfault.test c2b43d01213b389a3f518e90775fca2120812ba51e50444c4066962263e45c11 F ext/session/sessionfault2.test b0d6a7c1d7398a7e800d84657404909c7d385965ea8576dc79ed344c46fbf41c @@ -573,7 +574,7 @@ F ext/session/sessionrowid.test 85187c2f1b38861a5844868126f69f9ec62223a03449a98a F ext/session/sessionsize.test 8fcf4685993c3dbaa46a24183940ab9f5aa9ed0d23e5fb63bfffbdb56134b795 F ext/session/sessionstat1.test 5e718d5888c0c49bbb33a7a4f816366db85f59f6a4f97544a806421b85dc2dec F ext/session/sessionwor.test 6fd9a2256442cebde5b2284936ae9e0d54bde692d0f5fd009ecef8511f4cf3fc -F ext/session/sqlite3session.c a23436c2ecaddf0b0301460aa3ff2986f151eb36e7510509521467ce669e9d45 +F ext/session/sqlite3session.c c29ee85aba67f456aad2abfe8cdfded71f4c5c7e1d741c98938be245cba1727a F ext/session/sqlite3session.h 063e7bf7be2fff874456f452a224b5b3013b25682d108933b0351c93a1279b9c F ext/session/test_session.c 21524e86610986c8296ad08e71a1af1804b823b5830ed1189623b15ceb886c7c F ext/wasm/GNUmakefile 65feef4ec48e62249f90278c4c08a3fe3c69e2461ff560b61c03cd73606e0949 @@ -2205,8 +2206,11 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c -P 622882529558b4779dfb7246bd5a9de776555c8f940bb941397fb56fb9f97e43 -R d24f0da7d2c47dc861907e18b0a3ec5b -U drh -Z d5e5c346537f43415ecc1cfb3b15b0e6 +P ac3a958b0ab7766544bb406aa990668d2235ab26fb68c75ded3f71273d97b18c +R bc7ae802611a6dcb6a03884dbae18d5f +T *branch * session-update-loops +T *sym-session-update-loops * +T -sym-trunk * +U dan +Z b8bca31cc23b62b1a53ed4be75c6b16d # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.tags b/manifest.tags index bec971799f..6ff3ecbb01 100644 --- a/manifest.tags +++ b/manifest.tags @@ -1,2 +1,2 @@ -branch trunk -tag trunk +branch session-update-loops +tag session-update-loops diff --git a/manifest.uuid b/manifest.uuid index 9cf5dd0088..0b26bbc82c 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -ac3a958b0ab7766544bb406aa990668d2235ab26fb68c75ded3f71273d97b18c +5f8010599fb33fbe1dadc894a679056a5d38040ab4f09772cd59827941e9348b From a497743e2434aac3056d8818e470cf1113b8de0c Mon Sep 17 00:00:00 2001 From: dan Date: Mon, 18 May 2026 20:34:56 +0000 Subject: [PATCH 2/5] Fix a problem applying changesets that contain multiple UPDATE constraint loops. FossilOrigin-Name: 8e89aafab1b8fa7d59ac98c39b7f1c1cc23a98ba868a5dac83dee63688786a3f --- ext/session/sessionconflict2.test | 183 +++++++++++++++++++++++++++++- ext/session/sqlite3session.c | 1 + manifest | 17 ++- manifest.uuid | 2 +- 4 files changed, 186 insertions(+), 17 deletions(-) diff --git a/ext/session/sessionconflict2.test b/ext/session/sessionconflict2.test index d6fcde73ab..f0301428d1 100755 --- a/ext/session/sessionconflict2.test +++ b/ext/session/sessionconflict2.test @@ -20,12 +20,6 @@ ifcapable !session {finish_test; return} set testprefix sessionconflict2 -#db close -#sqlite3_shutdown -#test_sqlite3_log log -#proc log {code msg} { puts "LOG $code $msg" } -#sqlite3 db test.db - forcedelete test.db2 sqlite3 db2 test.db2 @@ -62,7 +56,184 @@ do_execsql_test -db db2 1.3 { 3 3 3 1 } +#-------------------------------------------------------------------------- +reset_db +db2 close +forcedelete test.db2 +sqlite3 db2 test.db2 +do_test 2.0 { + do_common_sql { + CREATE TABLE t1(a PRIMARY KEY, b, c UNIQUE) WITHOUT ROWID; + INSERT INTO t1 VALUES(1, 1, 1); + INSERT INTO t1 VALUES(2, 2, 2); + INSERT INTO t1 VALUES(3, 3, 3); + } +} {} + +do_test 2.1 { + do_then_apply_sql { + UPDATE t1 SET c=NULL WHERE a=1; + UPDATE t1 SET c=1 WHERE a=3; + UPDATE t1 SET c=3 WHERE a=1; + } +} {} + +do_execsql_test -db db 2.2 { + SELECT * FROM t1 +} { + 1 1 3 + 2 2 2 + 3 3 1 +} + +do_execsql_test -db db2 2.3 { + SELECT * FROM t1 +} { + 1 1 3 + 2 2 2 + 3 3 1 +} + +#-------------------------------------------------------------------------- +reset_db +db2 close +forcedelete test.db2 +sqlite3 db2 test.db2 + +do_test 3.0 { + do_common_sql { + CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c UNIQUE); + INSERT INTO t1 VALUES(1, 1, 1); + INSERT INTO t1 VALUES(2, 2, 2); + INSERT INTO t1 VALUES(3, 3, 3); + } +} {} + +do_test 3.1 { + do_then_apply_sql { + UPDATE t1 SET c=NULL WHERE a=1; + UPDATE t1 SET c=1 WHERE a=3; + UPDATE t1 SET c=3 WHERE a=1; + } +} {} + +do_execsql_test -db db 3.2 { + SELECT rowid, * FROM t1 +} { + 1 1 1 3 + 2 2 2 2 + 3 3 3 1 +} + +do_execsql_test -db db2 3.3 { + SELECT rowid, * FROM t1 +} { + 1 1 1 3 + 2 2 2 2 + 3 3 3 1 +} + +#------------------------------------------------------------------------- +reset_db +db2 close +forcedelete test.db2 +sqlite3 db2 test.db2 + +set ::conflict_list [list] +proc xConflict {args} { + lappend ::conflict_list $args + return "OMIT" +} + +proc do_conflict_test {tn script clist} { + + uplevel [list do_test $tn.1 [subst -nocommands { + sqlite3session S db "main" + S attach * + eval {$script} + set ::changeset [S changeset] + S delete + }] {}] + + uplevel [list do_test $tn.2 { + set ::conflict_list [list] + sqlite3changeset_apply_v2 db2 $::changeset xConflict + set ::conflict_list + } [list {*}$clist]] +} + +do_test 4.0 { + do_common_sql { + CREATE TABLE t1(a INT PRIMARY KEY, b, c UNIQUE, d UNIQUE); + WITH s(i) AS ( + SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<10 + ) + INSERT INTO t1 SELECT i, i, i, i FROM s; + } +} {} + +proc swap {tbl pkcol valcol pk1 pk2} { + set val1 [db one "SELECT $valcol FROM $tbl WHERE $pkcol = \$pk1"] + set val2 [db one "SELECT $valcol FROM $tbl WHERE $pkcol = \$pk2"] + + db eval " + UPDATE $tbl SET $valcol = NULL WHERE $pkcol IN (\$pk1, \$pk2); + UPDATE $tbl SET $valcol = \$val2 WHERE $pkcol = \$pk1; + UPDATE $tbl SET $valcol = \$val1 WHERE $pkcol = \$pk2; + " +} + +do_conflict_test 4.1.1 { + swap t1 a c 4 5 + swap t1 a c 2 3 + swap t1 a c 8 1 +} { +} + +do_execsql_test -db db 4.1.2 { + SELECT a, c FROM t1 +} { + 1 8 2 3 3 2 4 5 5 4 6 6 7 7 8 1 9 9 10 10 +} +do_execsql_test -db db2 4.1.3 { + SELECT a, c FROM t1 +} { + 1 8 2 3 3 2 4 5 5 4 6 6 7 7 8 1 9 9 10 10 +} + +do_conflict_test 4.2.1 { + swap t1 a d 10 9 + swap t1 a d 8 7 + swap t1 a d 7 6 + swap t1 a d 5 4 + swap t1 a d 4 8 +} { +} + +do_execsql_test -db db 4.2.2 { + SELECT a, d FROM t1 +} { + 1 1 2 2 3 3 4 7 5 4 6 8 7 6 8 5 9 10 10 9 +} +do_execsql_test -db db2 4.2.3 { + SELECT a, d FROM t1 +} { + 1 1 2 2 3 3 4 7 5 4 6 8 7 6 8 5 9 10 10 9 +} + +do_execsql_test -db db2 4.3 { + INSERT INTO t1(a, b, c, d) VALUES(11, 11, 11, 11); +} + +do_conflict_test 4.3.1 { + swap t1 a c 3 6 + db eval { UPDATE t1 SET d=11 WHERE a=10; } + swap t1 a d 4 8 +} { + {UPDATE t1 CONSTRAINT {i 10 {} {} {} {} i 9} {{} {} {} {} {} {} i 11}} +} db2 close finish_test + diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c index e8fc626a3e..96de0caafb 100644 --- a/ext/session/sqlite3session.c +++ b/ext/session/sqlite3session.c @@ -5506,6 +5506,7 @@ static int sessionRetryConstraints( if( pApply->constraints.nBuf>=app.nBuf ){ break; } + iSkip = -1; } } diff --git a/manifest b/manifest index 8bf4b6321e..7029ee904e 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Update\sthe\ssession\smodule\sso\sthat\sit\scan\sapply\schangesets\scontaining\stwo\sor\smore\sUPDATE\schanges\sthat\sform\sa\sdependency\sloop\s-\sso\sthat\sno\ssingle\sUPDATE\scan\sbe\sapplied\sindependently\swithout\sviolating\sa\sconstraint. -D 2026-05-18T18:59:06.664 +C Fix\sa\sproblem\sapplying\schangesets\sthat\scontain\smultiple\sUPDATE\sconstraint\sloops. +D 2026-05-18T20:34:56.095 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -559,7 +559,7 @@ F ext/session/sessionblob.test 87faf667870b72f08e91969abd9f52a383ab7b514506ee194 F ext/session/sessionchange.test 6618cb1c1338a4b6df173b6ac42d09623fb71269962abf23ebb7617fe9f45a50 F ext/session/sessionchange2.test 8f59185216882adc8b34bb5ba63887459acf3df58493bcffa12e4d05ab6a6b85 F ext/session/sessionconflict.test 19e4a53795c4c930bfec49e809311e09b2a9e202d9446e56d7a8b139046a0c07 x -F ext/session/sessionconflict2.test 9d65dd1b23c6ddd94134260c68ac3a7513683fbcfecc4d32344a416db115efa9 x +F ext/session/sessionconflict2.test 79e4922450355f5ee3929d53a5d4158d0ed5cec284399eb8f5cb756176fa601b x F ext/session/sessiondiff.test e89f7aedcdd89e5ebac3a455224eb553a171e9586fc3e1e6a7b3388d2648ba8d F ext/session/sessionfault.test c2b43d01213b389a3f518e90775fca2120812ba51e50444c4066962263e45c11 F ext/session/sessionfault2.test b0d6a7c1d7398a7e800d84657404909c7d385965ea8576dc79ed344c46fbf41c @@ -574,7 +574,7 @@ F ext/session/sessionrowid.test 85187c2f1b38861a5844868126f69f9ec62223a03449a98a F ext/session/sessionsize.test 8fcf4685993c3dbaa46a24183940ab9f5aa9ed0d23e5fb63bfffbdb56134b795 F ext/session/sessionstat1.test 5e718d5888c0c49bbb33a7a4f816366db85f59f6a4f97544a806421b85dc2dec F ext/session/sessionwor.test 6fd9a2256442cebde5b2284936ae9e0d54bde692d0f5fd009ecef8511f4cf3fc -F ext/session/sqlite3session.c c29ee85aba67f456aad2abfe8cdfded71f4c5c7e1d741c98938be245cba1727a +F ext/session/sqlite3session.c b02885cbe7135283b8c908f9d0a4321324090e01d4fc91a5a35e90fec5c8ebcb F ext/session/sqlite3session.h 063e7bf7be2fff874456f452a224b5b3013b25682d108933b0351c93a1279b9c F ext/session/test_session.c 21524e86610986c8296ad08e71a1af1804b823b5830ed1189623b15ceb886c7c F ext/wasm/GNUmakefile 65feef4ec48e62249f90278c4c08a3fe3c69e2461ff560b61c03cd73606e0949 @@ -2206,11 +2206,8 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c -P ac3a958b0ab7766544bb406aa990668d2235ab26fb68c75ded3f71273d97b18c -R bc7ae802611a6dcb6a03884dbae18d5f -T *branch * session-update-loops -T *sym-session-update-loops * -T -sym-trunk * +P 5f8010599fb33fbe1dadc894a679056a5d38040ab4f09772cd59827941e9348b +R 0e16a12d6b8b38337384a7915987c0d2 U dan -Z b8bca31cc23b62b1a53ed4be75c6b16d +Z c71ece5751b3bebe9c49c58b00d28025 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 0b26bbc82c..6ef08e7595 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -5f8010599fb33fbe1dadc894a679056a5d38040ab4f09772cd59827941e9348b +8e89aafab1b8fa7d59ac98c39b7f1c1cc23a98ba868a5dac83dee63688786a3f From 1a0555610d427c130d7b91ea79114afe15841459 Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 19 May 2026 18:28:03 +0000 Subject: [PATCH 3/5] Fix problems with error handling when applying changesets with update loops. FossilOrigin-Name: f797925df05f1a53392dba416aa72067716bb6e38ebc679d313f8a98f89c5913 --- ext/session/sessionconflict2.test | 18 ++++++++++++++ ext/session/sessionfault3.test | 41 +++++++++++++++++++++++++++++++ ext/session/sqlite3session.c | 13 ++++++++-- manifest | 16 ++++++------ manifest.uuid | 2 +- 5 files changed, 79 insertions(+), 11 deletions(-) diff --git a/ext/session/sessionconflict2.test b/ext/session/sessionconflict2.test index f0301428d1..012277aa5c 100755 --- a/ext/session/sessionconflict2.test +++ b/ext/session/sessionconflict2.test @@ -234,6 +234,24 @@ do_conflict_test 4.3.1 { {UPDATE t1 CONSTRAINT {i 10 {} {} {} {} i 9} {{} {} {} {} {} {} i 11}} } +do_conflict_test 4.3.2 { + swap t1 a c 1 2 + swap t1 a c 3 4 + db eval { UPDATE t1 SET c=11 WHERE a=10; } + swap t1 a d 5 6 + swap t1 a d 4 5 +} { + {UPDATE t1 CONSTRAINT {i 10 {} {} i 10 {} {}} {{} {} {} {} i 11 {} {}}} +} + +do_conflict_test 4.3.3 { + swap t1 a c 1 2 + swap t1 a c 2 3 + swap t1 a c 3 4 + swap t1 a c 4 1 +} { +} + db2 close finish_test diff --git a/ext/session/sessionfault3.test b/ext/session/sessionfault3.test index f2bcb89417..3e7dc6167e 100644 --- a/ext/session/sessionfault3.test +++ b/ext/session/sessionfault3.test @@ -94,4 +94,45 @@ do_faultsim_test 2 -faults oom-t* -prep { catch { S delete } } +#------------------------------------------------------------------------- +reset_db +do_execsql_test 3.0 { + CREATE TABLE t1(a PRIMARY KEY, b UNIQUE); + INSERT INTO t1 VALUES(1, 'one'); + INSERT INTO t1 VALUES(2, 'two'); + INSERT INTO t1 VALUES(3, 'three'); + INSERT INTO t1 VALUES(4, 'four'); + INSERT INTO t1 VALUES(5, 'five'); +} +faultsim_save_and_close +faultsim_restore_and_reopen + +set C [changeset_from_sql { + UPDATE t1 SET b=NULL WHERE a IN (1, 3, 5); + UPDATE t1 SET b='three' WHERE a=1; + UPDATE t1 SET b='five' WHERE a=3; + UPDATE t1 SET b='one' WHERE a=5; +}] + +do_execsql_test 3.1 { + SELECT * FROM t1 +} { + 1 three 2 two 3 five 4 four 5 one +} + +proc xConflict {args} { + lappend ::conflict_list $args + return "OMIT" +} + +do_faultsim_test 3 -faults oom* -prep { + faultsim_restore_and_reopen + db eval {SELECT * FROM sqlite_schema} +} -body { + sqlite3changeset_apply_v2 db $::C xConflict + set {} {} +} -test { + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} +} + finish_test diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c index 87cc274d97..2542d1e30c 100644 --- a/ext/session/sqlite3session.c +++ b/ext/session/sqlite3session.c @@ -5206,7 +5206,12 @@ static int sessionRetryIterInit( pRet->abPK = pApply->abPK; sessionBufferGrow(&pRet->tblhdr, nByte, &rc); pRet->apValue = (sqlite3_value**)pRet->tblhdr.aBuf; - if( rc==SQLITE_OK ) memset(pRet->apValue, 0, nByte); + if( rc==SQLITE_OK ){ + memset(pRet->apValue, 0, nByte); + }else{ + sqlite3changeset_finalize(pRet); + pRet = 0; + } } *ppIter = pRet; @@ -5374,7 +5379,9 @@ static int sessionUpdateToDeleteInsert( ); } if( rc==SQLITE_OK && sqlite3_step(pSelect)!=SQLITE_ROW ){ - rc = SQLITE_ERROR; + sessionFinalizeStmt(pSelect, &rc); + if( rc==SQLITE_OK ) rc = SQLITE_ERROR; + pSelect = 0; } if( rc==SQLITE_OK ){ @@ -5529,6 +5536,8 @@ static int sessionRetryConstraints( if( rc==SQLITE_OK ){ rc = sqlite3_exec(db, "RELEASE update_op", 0, 0, 0); } + }else{ + sqlite3_finalize(pInsert); } sqlite3_free(cons.aBuf); diff --git a/manifest b/manifest index c01e686ccf..57deb36458 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Merge\strunk\schanges\sinto\sthis\sbranch. -D 2026-05-19T17:15:05.160 +C Fix\sproblems\swith\serror\shandling\swhen\sapplying\schangesets\swith\supdate\sloops. +D 2026-05-19T18:28:03.681 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -559,11 +559,11 @@ F ext/session/sessionblob.test 87faf667870b72f08e91969abd9f52a383ab7b514506ee194 F ext/session/sessionchange.test 6618cb1c1338a4b6df173b6ac42d09623fb71269962abf23ebb7617fe9f45a50 F ext/session/sessionchange2.test 8f59185216882adc8b34bb5ba63887459acf3df58493bcffa12e4d05ab6a6b85 F ext/session/sessionconflict.test 19e4a53795c4c930bfec49e809311e09b2a9e202d9446e56d7a8b139046a0c07 x -F ext/session/sessionconflict2.test 79e4922450355f5ee3929d53a5d4158d0ed5cec284399eb8f5cb756176fa601b x +F ext/session/sessionconflict2.test 795768e963c96078b7939f397824e26aede6e5563a0e42f7b11c12795f313f3a x F ext/session/sessiondiff.test e89f7aedcdd89e5ebac3a455224eb553a171e9586fc3e1e6a7b3388d2648ba8d F ext/session/sessionfault.test c2b43d01213b389a3f518e90775fca2120812ba51e50444c4066962263e45c11 F ext/session/sessionfault2.test b0d6a7c1d7398a7e800d84657404909c7d385965ea8576dc79ed344c46fbf41c -F ext/session/sessionfault3.test 9397819ec25b0960c5bc03c78613f9cb5cacc970f83e817aec1775c2a839a787 +F ext/session/sessionfault3.test aea5331fa6dbe5ca4e19826605e624c0e1767545411479f27c5ef82b41046925 F ext/session/sessioninvert.test 9018f6a7387ac745084b6374c5e1aa14d648b372e6e1181cfab3df632b662d26 x F ext/session/sessionmem.test f2a735db84a3e9e19f571033b725b0b2daf847f3f28b1da55a0c1a4e74f1de09 F ext/session/sessionnoact.test 4c7ae5c7d351cb5323bca62b6b095592ad24bd90a6713c178b62ab0063d23e19 @@ -574,7 +574,7 @@ F ext/session/sessionrowid.test 85187c2f1b38861a5844868126f69f9ec62223a03449a98a F ext/session/sessionsize.test 8fcf4685993c3dbaa46a24183940ab9f5aa9ed0d23e5fb63bfffbdb56134b795 F ext/session/sessionstat1.test 5e718d5888c0c49bbb33a7a4f816366db85f59f6a4f97544a806421b85dc2dec F ext/session/sessionwor.test 6fd9a2256442cebde5b2284936ae9e0d54bde692d0f5fd009ecef8511f4cf3fc -F ext/session/sqlite3session.c 740d9110e72b86fa807ce39e0f6caf161a7b20b3cecc5d07b342c4e428fafac6 +F ext/session/sqlite3session.c ed5a9930c6d4caefaab46c7725dcaf71dec315c39f5cae95326894e4a604cb4e F ext/session/sqlite3session.h 063e7bf7be2fff874456f452a224b5b3013b25682d108933b0351c93a1279b9c F ext/session/test_session.c 3773e750b5c751956fdbef41a998cc1ba02d59c3dede74e75866e3446a900e69 F ext/wasm/GNUmakefile 65feef4ec48e62249f90278c4c08a3fe3c69e2461ff560b61c03cd73606e0949 @@ -2206,8 +2206,8 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c -P 1f71b530c9e8ddce5b25ed0971447cd925f9194afc7a2f929429318a5fcb5a72 4d8c3a2919dc942a0a044ec7582a688e0f93e91f4a465bb94390fbe3ad1b50b4 -R 51bc5e2ac281a99f16d90b52f95bc1e8 +P e480e126fc1550948bd1a3635f0e3a862353dcd8b0f1921ab6e66d699110b477 +R 555931a9156d91c5622ca72d0db559c6 U dan -Z 7f1594d88d6b54223b3b8783d29a568d +Z 92799d9d997d3240b13f6af3f8d382c0 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index a4eee70fa9..d8afc51290 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -e480e126fc1550948bd1a3635f0e3a862353dcd8b0f1921ab6e66d699110b477 +f797925df05f1a53392dba416aa72067716bb6e38ebc679d313f8a98f89c5913 From 3ef3ecdf8afc3dd1c236310875b0d12551dfb6cb Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 19 May 2026 18:58:59 +0000 Subject: [PATCH 4/5] Remove new unreachable code from this branch. FossilOrigin-Name: 07f81e256fde5796c0d8bcedff8fad4368639386f40881c0844e9c86a69b6fad --- ext/session/sqlite3session.c | 12 +++--------- manifest | 12 ++++++------ manifest.uuid | 2 +- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c index 2542d1e30c..c76950ee45 100644 --- a/ext/session/sqlite3session.c +++ b/ext/session/sqlite3session.c @@ -5278,9 +5278,8 @@ static int sessionTableIsWithoutRowid(sqlite3 *db, const char *zTab, int *pbWR){ } if( rc==SQLITE_OK ){ - if( SQLITE_ROW==sqlite3_step(pList) ){ - *pbWR = sqlite3_column_int(pList, 4); - } + sqlite3_step(pList); + *pbWR = sqlite3_column_int(pList, 4); rc = sqlite3_finalize(pList); } @@ -5378,13 +5377,8 @@ static int sessionUpdateToDeleteInsert( pUp, sqlite3changeset_old, pApply->nCol, pApply->abPK, pSelect ); } - if( rc==SQLITE_OK && sqlite3_step(pSelect)!=SQLITE_ROW ){ - sessionFinalizeStmt(pSelect, &rc); - if( rc==SQLITE_OK ) rc = SQLITE_ERROR; - pSelect = 0; - } - if( rc==SQLITE_OK ){ + if( rc==SQLITE_OK && sqlite3_step(pSelect)==SQLITE_ROW ){ int iCol; for(iCol=0; iColnCol; iCol++){ sqlite3_value *pVal = pUp->apValue[iCol+pApply->nCol]; diff --git a/manifest b/manifest index 57deb36458..375ec62dac 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Fix\sproblems\swith\serror\shandling\swhen\sapplying\schangesets\swith\supdate\sloops. -D 2026-05-19T18:28:03.681 +C Remove\snew\sunreachable\scode\sfrom\sthis\sbranch. +D 2026-05-19T18:58:59.031 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -574,7 +574,7 @@ F ext/session/sessionrowid.test 85187c2f1b38861a5844868126f69f9ec62223a03449a98a F ext/session/sessionsize.test 8fcf4685993c3dbaa46a24183940ab9f5aa9ed0d23e5fb63bfffbdb56134b795 F ext/session/sessionstat1.test 5e718d5888c0c49bbb33a7a4f816366db85f59f6a4f97544a806421b85dc2dec F ext/session/sessionwor.test 6fd9a2256442cebde5b2284936ae9e0d54bde692d0f5fd009ecef8511f4cf3fc -F ext/session/sqlite3session.c ed5a9930c6d4caefaab46c7725dcaf71dec315c39f5cae95326894e4a604cb4e +F ext/session/sqlite3session.c 51d751a9303efba9bd6950023678db5e9846a032b8a11c4a9e16041e6674d5b9 F ext/session/sqlite3session.h 063e7bf7be2fff874456f452a224b5b3013b25682d108933b0351c93a1279b9c F ext/session/test_session.c 3773e750b5c751956fdbef41a998cc1ba02d59c3dede74e75866e3446a900e69 F ext/wasm/GNUmakefile 65feef4ec48e62249f90278c4c08a3fe3c69e2461ff560b61c03cd73606e0949 @@ -2206,8 +2206,8 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c -P e480e126fc1550948bd1a3635f0e3a862353dcd8b0f1921ab6e66d699110b477 -R 555931a9156d91c5622ca72d0db559c6 +P f797925df05f1a53392dba416aa72067716bb6e38ebc679d313f8a98f89c5913 +R 4105156918feeda0715614d62a8791c2 U dan -Z 92799d9d997d3240b13f6af3f8d382c0 +Z 4ffef87e4004799d9693461ff924abb8 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index d8afc51290..02f9a2b1c8 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -f797925df05f1a53392dba416aa72067716bb6e38ebc679d313f8a98f89c5913 +07f81e256fde5796c0d8bcedff8fad4368639386f40881c0844e9c86a69b6fad From f380599476317bacfb234926916028a69567614e Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 22 May 2026 14:22:14 +0000 Subject: [PATCH 5/5] Add the SQLITE_CHANGESETAPPLY_NOUPDATELOOP option to the sqlite3_changeset_apply_v2/3() method. To disable the extra processing to commit changesets that swap two or more values subject to a UNIQUE constraint between rows. FossilOrigin-Name: eba02092bcf1713a646ecf8febd53a83bc803cf1b17e2e4fc590c14d886a8d27 --- ext/session/sessionconflict2.test | 67 +++++++++++++++++++++++++------ ext/session/sqlite3session.c | 4 +- ext/session/sqlite3session.h | 12 ++++++ ext/session/test_session.c | 3 ++ manifest | 18 ++++----- manifest.uuid | 2 +- 6 files changed, 82 insertions(+), 24 deletions(-) diff --git a/ext/session/sessionconflict2.test b/ext/session/sessionconflict2.test index 012277aa5c..d3d28bb5d6 100755 --- a/ext/session/sessionconflict2.test +++ b/ext/session/sessionconflict2.test @@ -135,8 +135,8 @@ do_execsql_test -db db2 3.3 { } #------------------------------------------------------------------------- -reset_db db2 close +reset_db forcedelete test.db2 sqlite3 db2 test.db2 @@ -146,7 +146,7 @@ proc xConflict {args} { return "OMIT" } -proc do_conflict_test {tn script clist} { +proc do_conflict_test {tn bNoUpdateLoop script clist} { uplevel [list do_test $tn.1 [subst -nocommands { sqlite3session S db "main" @@ -156,11 +156,19 @@ proc do_conflict_test {tn script clist} { S delete }] {}] - uplevel [list do_test $tn.2 { - set ::conflict_list [list] - sqlite3changeset_apply_v2 db2 $::changeset xConflict - set ::conflict_list - } [list {*}$clist]] + if {$bNoUpdateLoop} { + uplevel [list do_test $tn.2.no { + set ::conflict_list [list] + sqlite3changeset_apply_v2 -noupdateloop db2 $::changeset xConflict + set ::conflict_list + } [list {*}$clist]] + } else { + uplevel [list do_test $tn.2 { + set ::conflict_list [list] + sqlite3changeset_apply_v2 db2 $::changeset xConflict + set ::conflict_list + } [list {*}$clist]] + } } do_test 4.0 { @@ -184,7 +192,7 @@ proc swap {tbl pkcol valcol pk1 pk2} { " } -do_conflict_test 4.1.1 { +do_conflict_test 4.1.1 0 { swap t1 a c 4 5 swap t1 a c 2 3 swap t1 a c 8 1 @@ -202,7 +210,7 @@ do_execsql_test -db db2 4.1.3 { 1 8 2 3 3 2 4 5 5 4 6 6 7 7 8 1 9 9 10 10 } -do_conflict_test 4.2.1 { +do_conflict_test 4.2.1 0 { swap t1 a d 10 9 swap t1 a d 8 7 swap t1 a d 7 6 @@ -226,7 +234,7 @@ do_execsql_test -db db2 4.3 { INSERT INTO t1(a, b, c, d) VALUES(11, 11, 11, 11); } -do_conflict_test 4.3.1 { +do_conflict_test 4.3.1 0 { swap t1 a c 3 6 db eval { UPDATE t1 SET d=11 WHERE a=10; } swap t1 a d 4 8 @@ -234,7 +242,7 @@ do_conflict_test 4.3.1 { {UPDATE t1 CONSTRAINT {i 10 {} {} {} {} i 9} {{} {} {} {} {} {} i 11}} } -do_conflict_test 4.3.2 { +do_conflict_test 4.3.2 0 { swap t1 a c 1 2 swap t1 a c 3 4 db eval { UPDATE t1 SET c=11 WHERE a=10; } @@ -244,14 +252,47 @@ do_conflict_test 4.3.2 { {UPDATE t1 CONSTRAINT {i 10 {} {} i 10 {} {}} {{} {} {} {} i 11 {} {}}} } -do_conflict_test 4.3.3 { +do_conflict_test 4.3.3 0 { swap t1 a c 1 2 swap t1 a c 2 3 swap t1 a c 3 4 swap t1 a c 4 1 } { } - db2 close + +#------------------------------------------------------------------------- +reset_db +forcedelete test.db2 +sqlite3 db2 test.db2 + +do_test 5.0 { + do_common_sql { + CREATE TABLE t1(a INT PRIMARY KEY, b UNIQUE); + INSERT INTO t1 VALUES('one', 'one'); + INSERT INTO t1 VALUES('two', 'two'); + INSERT INTO t1 VALUES('three', 'three'); + INSERT INTO t1 VALUES('four', 'four'); + INSERT INTO t1 VALUES('five', 'five'); + } +} {} + +do_conflict_test 5.1 1 { +} { +} + +do_conflict_test 5.2 1 { + swap t1 a b one two +} { + {UPDATE t1 CONSTRAINT {t two t two} {{} {} t one}} + {UPDATE t1 CONSTRAINT {t one t one} {{} {} t two}} +} + +do_conflict_test 5.2 0 { + swap t1 a b four five +} { +} + + finish_test diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c index 544e18e323..7e914150e9 100644 --- a/ext/session/sqlite3session.c +++ b/ext/session/sqlite3session.c @@ -4364,6 +4364,7 @@ struct SessionApplyCtx { u8 bRebaseStarted; /* If table header is already in rebase */ u8 bRebase; /* True to collect rebase information */ u8 bIgnoreNoop; /* True to ignore no-op conflicts */ + u8 bNoUpdateLoop; /* No update-loop processing */ int bRowid; char *zErr; /* Error message, if any */ }; @@ -5472,7 +5473,7 @@ static int sessionRetryConstraints( } /* Step (2) */ - while( rc==SQLITE_OK && pApply->constraints.nBuf ){ + while( rc==SQLITE_OK && pApply->constraints.nBuf && !pApply->bNoUpdateLoop ){ SessionBuffer cons = {0, 0, 0}; sqlite3_changeset_iter *pUp = 0; sqlite3_stmt *pInsert = 0; @@ -5601,6 +5602,7 @@ static int sessionChangesetApply( sApply.bRebase = (ppRebase && pnRebase); sApply.bInvertConstraints = !!(flags & SQLITE_CHANGESETAPPLY_INVERT); sApply.bIgnoreNoop = !!(flags & SQLITE_CHANGESETAPPLY_IGNORENOOP); + sApply.bNoUpdateLoop = !!(flags & SQLITE_CHANGESETAPPLY_NOUPDATELOOP); if( (flags & SQLITE_CHANGESETAPPLY_NOSAVEPOINT)==0 ){ rc = sqlite3_exec(db, "SAVEPOINT changeset_apply", 0, 0, 0); } diff --git a/ext/session/sqlite3session.h b/ext/session/sqlite3session.h index fb2336d326..045a1ffeef 100644 --- a/ext/session/sqlite3session.h +++ b/ext/session/sqlite3session.h @@ -1366,11 +1366,23 @@ int sqlite3changeset_apply_v3( ** database behave as if they were declared with "ON UPDATE NO ACTION ON ** DELETE NO ACTION", even if they are actually CASCADE, RESTRICT, SET NULL ** or SET DEFAULT. +** +**
SQLITE_CHANGESETAPPLY_NOUPDATELOOP
+** Sometimes, a changeset contains two or more update statements such that +** although after applying all updates the database will contain no +** constraint violations, no single update can be applied before the others. +** The simplest example of this is a pair of UPDATEs that have "swapped" +** two column values with a UNIQUE constraint. +**

+** Usually, sqlite3changeset_apply() and similar functions work hard to try +** to find a way to apply such a changeset. However, if this flag is set, +** then all such updates are considered CONSTRAINT conflicts. */ #define SQLITE_CHANGESETAPPLY_NOSAVEPOINT 0x0001 #define SQLITE_CHANGESETAPPLY_INVERT 0x0002 #define SQLITE_CHANGESETAPPLY_IGNORENOOP 0x0004 #define SQLITE_CHANGESETAPPLY_FKNOACTION 0x0008 +#define SQLITE_CHANGESETAPPLY_NOUPDATELOOP 0x0010 /* ** CAPI3REF: Constants Passed To The Conflict Handler diff --git a/ext/session/test_session.c b/ext/session/test_session.c index bb8563aabb..7ede0bb426 100644 --- a/ext/session/test_session.c +++ b/ext/session/test_session.c @@ -914,6 +914,9 @@ static int SQLITE_TCLAPI testSqlite3changesetApply( } else if( n>2 && n<=11 && 0==sqlite3_strnicmp("-ignorenoop", z1, n) ){ flags |= SQLITE_CHANGESETAPPLY_IGNORENOOP; + } + else if( n>3 && n<=13 && 0==sqlite3_strnicmp("-noupdateloop", z1, n) ){ + flags |= SQLITE_CHANGESETAPPLY_NOUPDATELOOP; }else{ break; } diff --git a/manifest b/manifest index 54314d7740..c20c48221d 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Merge\slatest\strunk\schanges\swith\sthis\sbranch. -D 2026-05-21T15:13:17.189 +C Add\sthe\sSQLITE_CHANGESETAPPLY_NOUPDATELOOP\soption\sto\sthe\ssqlite3_changeset_apply_v2/3()\smethod.\sTo\sdisable\sthe\sextra\sprocessing\sto\scommit\schangesets\sthat\sswap\stwo\sor\smore\svalues\ssubject\sto\sa\sUNIQUE\sconstraint\sbetween\srows. +D 2026-05-22T14:22:14.502 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -559,7 +559,7 @@ F ext/session/sessionblob.test 87faf667870b72f08e91969abd9f52a383ab7b514506ee194 F ext/session/sessionchange.test 6618cb1c1338a4b6df173b6ac42d09623fb71269962abf23ebb7617fe9f45a50 F ext/session/sessionchange2.test 8f59185216882adc8b34bb5ba63887459acf3df58493bcffa12e4d05ab6a6b85 F ext/session/sessionconflict.test 19e4a53795c4c930bfec49e809311e09b2a9e202d9446e56d7a8b139046a0c07 x -F ext/session/sessionconflict2.test 795768e963c96078b7939f397824e26aede6e5563a0e42f7b11c12795f313f3a x +F ext/session/sessionconflict2.test d7f4caf59360dbca8a4698b9a3a322adf6f547f810ad41d131cacb730e02dd5e x F ext/session/sessiondiff.test e89f7aedcdd89e5ebac3a455224eb553a171e9586fc3e1e6a7b3388d2648ba8d F ext/session/sessionfault.test c2b43d01213b389a3f518e90775fca2120812ba51e50444c4066962263e45c11 F ext/session/sessionfault2.test b0d6a7c1d7398a7e800d84657404909c7d385965ea8576dc79ed344c46fbf41c @@ -574,9 +574,9 @@ F ext/session/sessionrowid.test 85187c2f1b38861a5844868126f69f9ec62223a03449a98a F ext/session/sessionsize.test 8fcf4685993c3dbaa46a24183940ab9f5aa9ed0d23e5fb63bfffbdb56134b795 F ext/session/sessionstat1.test 5e718d5888c0c49bbb33a7a4f816366db85f59f6a4f97544a806421b85dc2dec F ext/session/sessionwor.test 6fd9a2256442cebde5b2284936ae9e0d54bde692d0f5fd009ecef8511f4cf3fc -F ext/session/sqlite3session.c 6d3d284fa35e01a68db8d6f1aae0fd83a92c69d4442d6b88aed09b556fc2b707 -F ext/session/sqlite3session.h 063e7bf7be2fff874456f452a224b5b3013b25682d108933b0351c93a1279b9c -F ext/session/test_session.c 3773e750b5c751956fdbef41a998cc1ba02d59c3dede74e75866e3446a900e69 +F ext/session/sqlite3session.c 9d1cce13a48d821a31b36d99123ab25da87c3ae8b3bb96a926dfcc233a35ba9c +F ext/session/sqlite3session.h ca7c4422c1514a95056cc8d333217df6b1829d39058126b1de85d10cd62d7a9c +F ext/session/test_session.c 05c1f90c04de5474158bf8f7712a6f7a1d47477ce0402bbe0e55fc4a9ef1f49b F ext/wasm/GNUmakefile 65feef4ec48e62249f90278c4c08a3fe3c69e2461ff560b61c03cd73606e0949 F ext/wasm/README-dist.txt f01081a850ce38a56706af6b481e3a7878e24e42b314cfcd4b129f0f8427066a F ext/wasm/README.md 2e87804e12c98f1d194b7a06162a88441d33bb443efcfe00dc6565a780d2f259 @@ -2206,8 +2206,8 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c -P 07f81e256fde5796c0d8bcedff8fad4368639386f40881c0844e9c86a69b6fad 869a51ae84dfaaf824c872e4b3024f35eea7fa67bb584759a2d42ebf8404ef6e -R f21e35a31a0e9f095263f7664daa2ccc +P c6226d508d6af28fcd42fecd482014c3e284923871d3d8f5177f728cbf3457c5 +R 1eabf6c03fde51e4f558b57e68078de8 U dan -Z 7d6fcd668efb6e232e66f35845c58a26 +Z 7d1f8058359dfe62c0d29a8d06d235a4 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index b90b4bd3bd..5376a6e7b6 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -c6226d508d6af28fcd42fecd482014c3e284923871d3d8f5177f728cbf3457c5 +eba02092bcf1713a646ecf8febd53a83bc803cf1b17e2e4fc590c14d886a8d27