From f380599476317bacfb234926916028a69567614e Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 22 May 2026 14:22:14 +0000 Subject: [PATCH] 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 -- 2.47.3