diff --git a/ext/session/session1.test b/ext/session/session1.test index fc74a1e84e..7aebe8c000 100644 --- a/ext/session/session1.test +++ b/ext/session/session1.test @@ -20,17 +20,6 @@ ifcapable !session {finish_test; return} set testprefix session1 -proc do_changeset_invert_test {tn session res} { - set r [list] - foreach x $res {lappend r $x} - uplevel do_test $tn [list [subst -nocommands { - set x [list] - set changeset [sqlite3changeset_invert [$session changeset]] - sqlite3session_foreach c [set changeset] { lappend x [set c] } - set x - }]] [list $r] -} - do_execsql_test 1.0 { CREATE TABLE t1(x PRIMARY KEY, y); INSERT INTO t1 VALUES('abc', 'def'); diff --git a/ext/session/session2.test b/ext/session/session2.test index 0035e39dbf..cd8c2869e7 100644 --- a/ext/session/session2.test +++ b/ext/session/session2.test @@ -298,8 +298,8 @@ foreach {tn sql changeset} { DELETE FROM t1 WHERE 1; UPDATE t2 SET x = 34; } { - {UPDATE t2 0 .X {i 36 i 37} {i 34 {} {}}} {DELETE t1 0 X {i 35} {}} + {UPDATE t2 0 .X {i 36 i 37} {i 34 {} {}}} } } { do_iterator_test 5.$tn * $sql $changeset @@ -413,8 +413,8 @@ do_iterator_test 6.1.8 * { SELECT indirect(0); UPDATE t4 SET a = 'two' WHERE b = 1; } { - {INSERT t4 0 .X {} {t two i 1}} {INSERT t3 1 .X {} {t two i 1}} + {INSERT t4 0 .X {} {t two i 1}} } sqlite3session S db main diff --git a/ext/session/sessionB.test b/ext/session/sessionB.test index f1af5d213c..eae41f76ad 100644 --- a/ext/session/sessionB.test +++ b/ext/session/sessionB.test @@ -31,16 +31,6 @@ set testprefix sessionB # 3.*: Test that sqlite3changeset_invert() works with patchset blobs. # Correct behaviour is to return SQLITE_CORRUPT. -proc do_patchset_test {tn session res} { - set r [list] - foreach x $res {lappend r $x} - uplevel do_test $tn [list [subst -nocommands { - set x [list] - sqlite3session_foreach c [$session patchset] { lappend x [set c] } - set x - }]] [list $r] -} - proc do_sql2patchset_test {tn sql res} { sqlite3session S db main S attach * diff --git a/ext/session/sessionF.test b/ext/session/sessionF.test new file mode 100644 index 0000000000..ba57bf85ec --- /dev/null +++ b/ext/session/sessionF.test @@ -0,0 +1,295 @@ +# 2015 June 02 +# +# 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 the sessions module. +# Specifically, it tests that tables appear in the correct order +# within changesets and patchsets. +# + + + +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 sessionF + +# +# Test plan: +# +# 1.*: Test that sqlite3session_changeset() and sqlite3session_patchset() +# output tables in the right order. +# +# 2.*: Test that sqlite3session_invert() does not modify the order of +# tables within a changeset. +# +# 3.*: Test that sqlite3session_concat outputs tables in the right order. +# + +# Create a db schema to use. +# +do_execsql_test 1.0 { + CREATE TABLE t3(e PRIMARY KEY, f); + CREATE TABLE t1(a PRIMARY KEY, b); + CREATE TABLE t2(c PRIMARY KEY, d); +} + +#----------------------------------------------------------------------- +# 1.* - changeset() and patchset(). +# + +foreach {tn setup result} { + 1 { + S attach * + } { + {INSERT t2 0 X. {} {i 2 t two}} + {INSERT t1 0 X. {} {i 1 t one}} + {INSERT t3 0 X. {} {i 3 t three}} + } + + 2 { + S attach t1 + S attach * + } { + {INSERT t1 0 X. {} {i 1 t one}} + {INSERT t2 0 X. {} {i 2 t two}} + {INSERT t3 0 X. {} {i 3 t three}} + } + + 3 { + S attach t3 + S attach t2 + S attach t1 + } { + {INSERT t3 0 X. {} {i 3 t three}} + {INSERT t2 0 X. {} {i 2 t two}} + {INSERT t1 0 X. {} {i 1 t one}} + } +} { + execsql { + DELETE FROM t1; + DELETE FROM t2; + DELETE FROM t3; + } + sqlite3session S db main + eval $setup + + do_execsql_test 1.$tn.1 { + INSERT INTO t2 VALUES(2, 'two'); + INSERT INTO t1 VALUES(1, 'one'); + INSERT INTO t3 VALUES(3, 'three'); + } + + do_changeset_test 1.1.$tn.2 S $result + do_patchset_test 1.1.$tn.3 S $result + + S delete +} + +foreach {tn setup result} { + 1 { + S attach * + } { + {INSERT t2 0 X. {} {i 4 t four}} + {INSERT t2 0 X. {} {i 5 t five}} + {INSERT t1 0 X. {} {i 1 t one}} + {INSERT t3 0 X. {} {i 6 t six}} + } + + 2 { + S attach t1 + S attach * + } { + {INSERT t1 0 X. {} {i 1 t one}} + {INSERT t2 0 X. {} {i 4 t four}} + {INSERT t2 0 X. {} {i 5 t five}} + {INSERT t3 0 X. {} {i 6 t six}} + } + + 3 { + S attach t3 + S attach t2 + S attach t1 + } { + {INSERT t3 0 X. {} {i 6 t six}} + {INSERT t2 0 X. {} {i 4 t four}} + {INSERT t2 0 X. {} {i 5 t five}} + {INSERT t1 0 X. {} {i 1 t one}} + } +} { + execsql { + DELETE FROM t1; + DELETE FROM t2; + DELETE FROM t3; + } + sqlite3session S db main + eval $setup + + do_execsql_test 1.$tn.1 { + INSERT INTO t2 VALUES(2, 'two'); + INSERT INTO t1 VALUES(1, 'one'); + DELETE FROM t2; + INSERT INTO t2 VALUES(4, 'four'); + INSERT INTO t2 VALUES(5, 'five'); + INSERT INTO t3 VALUES(6, 'six'); + } + + do_changeset_test 1.2.$tn.2 S $result + do_patchset_test 1.2.$tn.2 S $result + + S delete +} + +#------------------------------------------------------------------------- +# 2.* - invert() +# + +foreach {tn setup result} { + 1 { + S attach * + } { + {DELETE t2 0 X. {i 4 t four} {}} + {DELETE t2 0 X. {i 5 t five} {}} + {DELETE t1 0 X. {i 1 t one} {}} + {DELETE t3 0 X. {i 6 t six} {}} + } + + 2 { + S attach t1 + S attach * + } { + {DELETE t1 0 X. {i 1 t one} {}} + {DELETE t2 0 X. {i 4 t four} {}} + {DELETE t2 0 X. {i 5 t five} {}} + {DELETE t3 0 X. {i 6 t six} {}} + } + + 3 { + S attach t3 + S attach t2 + S attach t1 + } { + {DELETE t3 0 X. {i 6 t six} {}} + {DELETE t2 0 X. {i 4 t four} {}} + {DELETE t2 0 X. {i 5 t five} {}} + {DELETE t1 0 X. {i 1 t one} {}} + } +} { + execsql { + DELETE FROM t1; + DELETE FROM t2; + DELETE FROM t3; + } + sqlite3session S db main + eval $setup + + do_execsql_test 1.$tn.1 { + INSERT INTO t2 VALUES(2, 'two'); + INSERT INTO t1 VALUES(1, 'one'); + DELETE FROM t2; + INSERT INTO t2 VALUES(4, 'four'); + INSERT INTO t2 VALUES(5, 'five'); + INSERT INTO t3 VALUES(6, 'six'); + } + + do_changeset_invert_test 2.$tn.2 S $result + + S delete +} + +#------------------------------------------------------------------------- +# 3.* - concat() +# +foreach {tn setup1 sql1 setup2 sql2 result} { + 1 { + S attach * + } { + INSERT INTO t1 VALUES(1, 'one'); + INSERT INTO t2 VALUES(2, 'two'); + } { + S attach t2 + S attach t1 + } { + INSERT INTO t1 VALUES(3, 'three'); + INSERT INTO t2 VALUES(4, 'four'); + } { + {INSERT t1 0 X. {} {i 1 t one}} + {INSERT t1 0 X. {} {i 3 t three}} + {INSERT t2 0 X. {} {i 2 t two}} + {INSERT t2 0 X. {} {i 4 t four}} + } + + 1 { + S attach t2 + S attach t1 + } { + INSERT INTO t1 VALUES(1, 'one'); + INSERT INTO t2 VALUES(2, 'two'); + } { + S attach * + } { + INSERT INTO t1 VALUES(3, 'three'); + INSERT INTO t2 VALUES(4, 'four'); + } { + {INSERT t2 0 X. {} {i 2 t two}} + {INSERT t2 0 X. {} {i 4 t four}} + {INSERT t1 0 X. {} {i 1 t one}} + {INSERT t1 0 X. {} {i 3 t three}} + } + + 1 { + S attach * + } { + INSERT INTO t2 VALUES(2, 'two'); + } { + S attach * + } { + INSERT INTO t1 VALUES(3, 'three'); + INSERT INTO t2 VALUES(4, 'four'); + INSERT INTO t3 VALUES(5, 'five'); + } { + {INSERT t2 0 X. {} {i 2 t two}} + {INSERT t2 0 X. {} {i 4 t four}} + {INSERT t1 0 X. {} {i 3 t three}} + {INSERT t3 0 X. {} {i 5 t five}} + } + +} { + execsql { + DELETE FROM t1; + DELETE FROM t2; + DELETE FROM t3; + } + sqlite3session S db main + eval $setup1 + execsql $sql1 + set c1 [S changeset] + S delete + + sqlite3session S db main + eval $setup2 + execsql $sql2 + set c2 [S changeset] + S delete + + set res [list] + sqlite3session_foreach x [sqlite3changeset_concat $c1 $c2] { + lappend res $x + } + + do_test 3.$tn { set res } [list {*}$result] +} + + +finish_test + diff --git a/ext/session/session_common.tcl b/ext/session/session_common.tcl index f27ad11dc0..38eebec0a0 100644 --- a/ext/session/session_common.tcl +++ b/ext/session/session_common.tcl @@ -9,6 +9,28 @@ proc do_changeset_test {tn session res} { }]] [list $r] } +proc do_patchset_test {tn session res} { + set r [list] + foreach x $res {lappend r $x} + uplevel do_test $tn [list [subst -nocommands { + set x [list] + sqlite3session_foreach c [$session patchset] { lappend x [set c] } + set x + }]] [list $r] +} + + +proc do_changeset_invert_test {tn session res} { + set r [list] + foreach x $res {lappend r $x} + uplevel do_test $tn [list [subst -nocommands { + set x [list] + set changeset [sqlite3changeset_invert [$session changeset]] + sqlite3session_foreach c [set changeset] { lappend x [set c] } + set x + }]] [list $r] +} + proc do_conflict_test {tn args} { proc xConflict {args} { diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c index 0937c9bb32..546a22a072 100644 --- a/ext/session/sqlite3session.c +++ b/ext/session/sqlite3session.c @@ -1188,7 +1188,7 @@ static int sessionFindTable( ){ rc = sqlite3session_attach(pSession, zName); if( rc==SQLITE_OK ){ - pRet = pSession->pTable; + for(pRet=pSession->pTable; pRet->pNext; pRet=pRet->pNext); assert( 0==sqlite3_strnicmp(pRet->zName, zName, nName+1) ); } } @@ -1675,12 +1675,17 @@ int sqlite3session_attach( if( !pTab ){ rc = SQLITE_NOMEM; }else{ - /* Populate the new SessionTable object and link it into the list. */ + /* Populate the new SessionTable object and link it into the list. + ** The new object must be linked onto the end of the list, not + ** simply added to the start of it in order to ensure that tables + ** appear in the correct order when a changeset or patchset is + ** eventually generated. */ + SessionTable **ppTab; memset(pTab, 0, sizeof(SessionTable)); pTab->zName = (char *)&pTab[1]; memcpy(pTab->zName, zName, nName+1); - pTab->pNext = pSession->pTable; - pSession->pTable = pTab; + for(ppTab=&pSession->pTable; *ppTab; ppTab=&(*ppTab)->pNext); + *ppTab = pTab; } } } @@ -4220,19 +4225,26 @@ static int sessionChangesetToHash( if( 0==sqlite3_strnicmp(pTab->zName, zNew, nNew+1) ) break; } if( !pTab ){ + SessionTable **ppTab; + pTab = sqlite3_malloc(sizeof(SessionTable) + nCol + nNew+1); if( !pTab ){ rc = SQLITE_NOMEM; break; } memset(pTab, 0, sizeof(SessionTable)); - pTab->pNext = pGrp->pList; pTab->nCol = nCol; pTab->abPK = (u8*)&pTab[1]; memcpy(pTab->abPK, abPK, nCol); pTab->zName = (char*)&pTab->abPK[nCol]; memcpy(pTab->zName, zNew, nNew+1); - pGrp->pList = pTab; + + /* The new object must be linked on to the end of the list, not + ** simply added to the start of it. This is to ensure that the + ** tables within the output of sqlite3changegroup_output() are in + ** the right order. */ + for(ppTab=&pGrp->pList; *ppTab; ppTab=&(*ppTab)->pNext); + *ppTab = pTab; }else if( pTab->nCol!=nCol || memcmp(pTab->abPK, abPK, nCol) ){ rc = SQLITE_SCHEMA; break; diff --git a/ext/session/sqlite3session.h b/ext/session/sqlite3session.h index f2df143e1a..71f02159cf 100644 --- a/ext/session/sqlite3session.h +++ b/ext/session/sqlite3session.h @@ -206,6 +206,14 @@ void sqlite3session_table_filter( ** a database with a compatible schema using the [sqlite3changeset_apply()] ** API. ** +** Within a changeset generated by this function, all changes related to a +** single table are grouped together. In other words, when iterating through +** a changeset or when applying a changeset to a database, all changes related +** to a single table are processed before moving on to the next table. Tables +** are sorted in the same order in which they were attached (or auto-attached) +** to the sqlite3_session object. The order in which the changes related to +** a single table are stored is undefined. +** ** Following a successful call to this function, it is the responsibility of ** the caller to eventually free the buffer that *ppChangeset points to using ** [sqlite3_free()]. @@ -358,6 +366,11 @@ int sqlite3session_diff( ** SQLITE_CHANGESET_DATA conflicts can be detected or reported if a patchset ** is passed to the sqlite3changeset_apply() API. Other conflict types work ** in the same way as for changesets. +** +** Changes within a patchset are ordered in the same way as for changesets +** generated by the sqlite3session_changeset() function (i.e. all changes for +** a single table are grouped together, tables appear in the order in which +** they were attached to the session object). */ int sqlite3session_patchset( sqlite3_session *pSession, /* Session object */ @@ -659,6 +672,9 @@ int sqlite3changeset_finalize(sqlite3_changeset_iter *pIter); **