]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Update the session module so that it can apply changesets containing two or more...
authordan <Dan Kennedy>
Tue, 2 Jun 2026 18:23:55 +0000 (18:23 +0000)
committerdan <Dan Kennedy>
Tue, 2 Jun 2026 18:23:55 +0000 (18:23 +0000)
FossilOrigin-Name: 919d393a3bc483bf58be1f8d6c2ef70f570d63cc9ad8d8df6a6562fb270ea7e5

ext/session/sessionG.test
ext/session/sessionconflict2.test [new file with mode: 0755]
ext/session/sessionfault3.test
ext/session/sqlite3session.c
ext/session/sqlite3session.h
ext/session/test_session.c
manifest
manifest.uuid

index 1ebcc926a5f4e26a271b0821aefb4d85893c6864..58713a5b62e7e5e609b1a8a19444255a896394a7 100644 (file)
@@ -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 (executable)
index 0000000..d3d28bb
--- /dev/null
@@ -0,0 +1,298 @@
+# 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
+
+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
+}
+
+#--------------------------------------------------------------------------
+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
+}
+
+#-------------------------------------------------------------------------
+db2 close
+reset_db
+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 bNoUpdateLoop script clist} {
+
+  uplevel [list do_test $tn.1 [subst -nocommands {
+    sqlite3session S db "main"
+    S attach *
+    eval {$script}
+    set ::changeset [S changeset]
+    S delete
+  }] {}]
+
+  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 {
+  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 0 {
+  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 0 {
+  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 0 {
+  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}}
+}
+
+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; }
+  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 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
+
index f2bcb894172864762d3421ed9b031c2cf282d126..3e7dc6167eef7cc3b4bdbe1f14d0b905ce966238 100644 (file)
@@ -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
index 3634013ac4dde538dc30946c709a767c670b1ba0..c94664cf843e307112736eb8a5f587a7417df399 100644 (file)
@@ -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; i<nCol; i++){
       if( cols.nBuf ) sessionAppendStr(&cols, ", ", &rc);
       sessionAppendIdent(&cols, azCol[i], &rc);
@@ -4355,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 */
 };
@@ -4928,7 +4938,7 @@ static int sessionConflictHandler(
       u8 *aBlob = &pIter->in.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 
     ){
@@ -5176,7 +5186,264 @@ 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);
+    }else{
+      sqlite3changeset_finalize(pRet);
+      pRet = 0;
+    }
+  }
+
+  *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 ){
+    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; ii<pApply->nCol; 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 ){
+    int iCol;
+    for(iCol=0; iCol<pApply->nCol; 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, 
@@ -5187,41 +5454,101 @@ 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
     );
+
+    sqlite3_free(cons.aBuf);
+    if( rc!=SQLITE_OK ) break;
+
+    /* 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 && !pApply->bNoUpdateLoop ){
+    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 ){
-      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);
+      cons = pApply->constraints;
 
-      while( rc==SQLITE_OK && SQLITE_ROW==sqlite3changeset_next(pIter2) ){
-        rc = sessionApplyOneWithRetry(db, pIter2, pApply, xConflict, pCtx);
+      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;
+        }
+        iSkip = -1;
       }
+    }
 
-      rc2 = sqlite3changeset_finalize(pIter2);
-      if( rc==SQLITE_OK ) rc = rc2;
+    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);
+      }
+    }else{
+      sqlite3_finalize(pInsert);
     }
-    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;
-    }
+  }
+
+  /* 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;
@@ -5275,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);
   }
index fb2336d3262285c5098c593a6c1fa72c5aaf9ed1..045a1ffeefab5a03f408161bfbd46d5e2a156728 100644 (file)
@@ -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.
+**
+** <dt>SQLITE_CHANGESETAPPLY_NOUPDATELOOP <dd>
+**   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.
+**   <p>
+**   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
index be516e5825baa9f502460f8536fdf7b82ee69a6d..47e50caa7416aa24a84073df54461d3635f5e716 100644 (file)
@@ -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;
       }
index 4e2ac9b887bc7f3b11f4063abd547dc7748579a3..e9edbcbd68fb239ac5f6a0ffbebec1e6d6579ee0 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Remove\sa\sNEVER()\sthat\sis\sactually\sreachable.\s\sThis\ssame\sNEVER()\swas\sremoved\nfrom\strunk\sat\scheck-in\s[0de3d95500b7ecd4].
-D 2026-06-02T13:44:21.252
+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-06-02T18:23:55.109
 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x
 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
@@ -544,7 +544,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
@@ -557,10 +557,11 @@ F ext/session/sessionblob.test 87faf667870b72f08e91969abd9f52a383ab7b514506ee194
 F ext/session/sessionchange.test 6618cb1c1338a4b6df173b6ac42d09623fb71269962abf23ebb7617fe9f45a50
 F ext/session/sessionchange2.test 6b5b7e3d1cc4ede43817f7fb68e3771aac4aa6500adb21a458b3e5a9fd841f83
 F ext/session/sessionconflict.test 19e4a53795c4c930bfec49e809311e09b2a9e202d9446e56d7a8b139046a0c07 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
-F ext/session/sessionfault3.test 9397819ec25b0960c5bc03c78613f9cb5cacc970f83e817aec1775c2a839a787
+F ext/session/sessionfault3.test aea5331fa6dbe5ca4e19826605e624c0e1767545411479f27c5ef82b41046925
 F ext/session/sessioninvert.test 7ccb7609a2c11e4e13e606df439bf3d484ba8e455d0bd3aa8d4828a940e1a242 x
 F ext/session/sessionmem.test f2a735db84a3e9e19f571033b725b0b2daf847f3f28b1da55a0c1a4e74f1de09
 F ext/session/sessionnoact.test 2cf060c12a7a23e663f0ec796561e58638c5c10a846653d37be886414b06ddc9
@@ -571,9 +572,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 e36c91f273e4d2ce11c9e3aaba160038c9703cda1feeb79a96bb00f3de1a6d5e
-F ext/session/sqlite3session.h 063e7bf7be2fff874456f452a224b5b3013b25682d108933b0351c93a1279b9c
-F ext/session/test_session.c 9435a0d2c67b6c693bbf943657eeb83198efe06f796de80a6fd563013fa20bcc
+F ext/session/sqlite3session.c 1010718d9d88eeb1efd1bf3d6bcbc85e598e9d5bb094c2d202515622d2233481
+F ext/session/sqlite3session.h ca7c4422c1514a95056cc8d333217df6b1829d39058126b1de85d10cd62d7a9c
+F ext/session/test_session.c 95fbf8fc721fdb1a0d00268f930d6763c5299ed766b317f9dd3cf9ca5262e337
 F ext/wasm/GNUmakefile 68c750f173106d9d63f12c1edf1256c6f4bad9894b155da5db64322f4912de4b
 F ext/wasm/README-dist.txt f01081a850ce38a56706af6b481e3a7878e24e42b314cfcd4b129f0f8427066a
 F ext/wasm/README.md 2e87804e12c98f1d194b7a06162a88441d33bb443efcfe00dc6565a780d2f259
@@ -2198,8 +2199,9 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee
 F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2
 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f
 F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c
-P 4c0455efe57d1e3c27327e942a1509de3eb7b0902bc3b90473e2cba8df90139b
-R 14c268820e8475568c33473b72a882db
-U drh
-Z 72720f8bcf018c13d516ed36dbb636da
+P 46e74947f155199f5ce2440e8d5f19849d0daeb1d9d47381b0f84388901eab4c
+Q +32c762bbb187e3bc964dcaad0949ebeb4da21331be842a5868feeac6088790ca
+R e809b7ef1093cc510f207b67ac790177
+U dan
+Z ca58af6e9f4882ad16a9a2a61f15e915
 # Remove this line to create a well-formed Fossil manifest.
index 8c59dda39dde6ea2e8b5679a462cb5e10768524e..980f76555bda8bb0167e348dcde0f71a8a96a5c0 100644 (file)
@@ -1 +1 @@
-46e74947f155199f5ce2440e8d5f19849d0daeb1d9d47381b0f84388901eab4c
+919d393a3bc483bf58be1f8d6c2ef70f570d63cc9ad8d8df6a6562fb270ea7e5