mirror of
https://github.com/sqlite/sqlite.git
synced 2026-10-23 21:49:02 +03:00
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: 32c762bbb187e3bc964dcaad0949ebeb4da21331be842a5868feeac6088790ca
This commit is contained in:
@@ -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}
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
298
ext/session/sessionconflict2.test
Executable file
298
ext/session/sessionconflict2.test
Executable file
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1547,6 +1547,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
|
||||
@@ -1569,9 +1579,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;
|
||||
}
|
||||
|
||||
@@ -2897,11 +2906,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
|
||||
);
|
||||
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 && !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 ){
|
||||
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;
|
||||
}
|
||||
iSkip = -1;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
24
manifest
24
manifest
@@ -1,5 +1,5 @@
|
||||
C Add\sa\sprototype\sAGENTS.md\sfile
|
||||
D 2026-05-22T11:15:25.905
|
||||
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-22T14:29:33.548
|
||||
F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x
|
||||
F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
|
||||
F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
|
||||
@@ -547,7 +547,7 @@ F ext/session/sessionC.test 7c9b5e90194b3b7ad8bcbc6fcbac77d2c572ec3566a30fce1e82
|
||||
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
|
||||
@@ -560,10 +560,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 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 9018f6a7387ac745084b6374c5e1aa14d648b372e6e1181cfab3df632b662d26 x
|
||||
F ext/session/sessionmem.test f2a735db84a3e9e19f571033b725b0b2daf847f3f28b1da55a0c1a4e74f1de09
|
||||
F ext/session/sessionnoact.test 2cf060c12a7a23e663f0ec796561e58638c5c10a846653d37be886414b06ddc9
|
||||
@@ -574,9 +575,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 7e0823eadf9005e98e06cfa6724cf3354a20e27e70fbaceae686441f399e9f08
|
||||
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 +2207,9 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee
|
||||
F tool/warnings.sh a554d13f6e5cf3760f041b87939e3d616ec6961859c3245e8ef701d1eafc2ca2
|
||||
F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f
|
||||
F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c
|
||||
P 9ac4a33a2932d353c4871fd8e09c10addf827f1fc3fc9380037d738cf2cd0353
|
||||
R 2bd6937d433d800bf75d925a3aef6528
|
||||
U drh
|
||||
Z 83c1080ffcb6a157fc9e6d16bda364e0
|
||||
P 3dd1181120b9910a96090b5b3cad5f0829fadeffeb6ffaad17031540a5749513 eba02092bcf1713a646ecf8febd53a83bc803cf1b17e2e4fc590c14d886a8d27
|
||||
R 1e0cffe8e9e92c4d63d8db26c0f0e206
|
||||
T +closed eba02092bcf1713a646ecf8febd53a83bc803cf1b17e2e4fc590c14d886a8d27
|
||||
U dan
|
||||
Z 8ebbbd55906ba3a7c1dd7b620b59bb89
|
||||
# Remove this line to create a well-formed Fossil manifest.
|
||||
|
||||
@@ -1 +1 @@
|
||||
3dd1181120b9910a96090b5b3cad5f0829fadeffeb6ffaad17031540a5749513
|
||||
32c762bbb187e3bc964dcaad0949ebeb4da21331be842a5868feeac6088790ca
|
||||
|
||||
Reference in New Issue
Block a user