Index: ext/session/session_common.tcl ================================================================== --- ext/session/session_common.tcl +++ ext/session/session_common.tcl @@ -167,5 +167,6 @@ proc changeset_to_list {c} { set list [list] sqlite3session_foreach elem $c { lappend list $elem } lsort $list } + Index: ext/session/sessionfault2.test ================================================================== --- ext/session/sessionfault2.test +++ ext/session/sessionfault2.test @@ -17,10 +17,12 @@ } source [file join [file dirname [info script]] session_common.tcl] source $testdir/tester.tcl ifcapable !session {finish_test; return} set testprefix sessionfault2 + +if 1 { do_execsql_test 1.0.0 { CREATE TABLE t1(a PRIMARY KEY, b UNIQUE); INSERT INTO t1 VALUES(1, 1); INSERT INTO t1 VALUES(2, 2); @@ -100,8 +102,183 @@ sqlite3changeset_apply db $::C xConflict } -test { faultsim_test_result {0 {}} {1 SQLITE_NOMEM} faultsim_integrity_check } + +#------------------------------------------------------------------------- +# OOM when collecting and apply a changeset that uses sqlite_stat1. +# +reset_db +forcedelete test.db2 +sqlite3 db2 test.db2 +do_common_sql { + CREATE TABLE t1(a PRIMARY KEY, b UNIQUE, c); + CREATE INDEX i1 ON t1(c); + INSERT INTO t1 VALUES(1, 2, 3); + INSERT INTO t1 VALUES(4, 5, 6); + INSERT INTO t1 VALUES(7, 8, 9); + CREATE TABLE t2(a, b, c); + INSERT INTO t2 VALUES(1, 2, 3); + INSERT INTO t2 VALUES(4, 5, 6); + INSERT INTO t2 VALUES(7, 8, 9); + ANALYZE; +} +faultsim_save_and_close +db2 close + +do_faultsim_test 1.1 -faults oom-* -prep { + catch {db2 close} + catch {db close} + faultsim_restore_and_reopen + sqlite3 db2 test.db2 +} -body { + do_then_apply_sql { + INSERT INTO sqlite_stat1 VALUES('x', 'y', 45); + UPDATE sqlite_stat1 SET stat = 123 WHERE tbl='t1' AND idx='i1'; + UPDATE sqlite_stat1 SET stat = 456 WHERE tbl='t2'; + } +} -test { + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} + faultsim_integrity_check + if {$testrc==0} { compare_db db db2 } +} + +#------------------------------------------------------------------------- +# OOM when collecting and using a rebase changeset. +# +reset_db +do_execsql_test 2.0 { + CREATE TABLE t3(a, b, c, PRIMARY KEY(b, c)); + CREATE TABLE t4(x PRIMARY KEY, y, z); + + INSERT INTO t3 VALUES(1, 2, 3); + INSERT INTO t3 VALUES(4, 2, 5); + INSERT INTO t3 VALUES(7, 2, 9); + + INSERT INTO t4 VALUES('a', 'b', 'c'); + INSERT INTO t4 VALUES('d', 'e', 'f'); + INSERT INTO t4 VALUES('g', 'h', 'i'); +} +faultsim_save_and_close + +proc xConflict {ret args} { return $ret } + +do_test 2.1 { + faultsim_restore_and_reopen + set C1 [changeset_from_sql { + INSERT INTO t3 VALUES(10, 11, 12); + UPDATE t4 SET y='j' WHERE x='g'; + DELETE FROM t4 WHERE x='a'; + }] + + faultsim_restore_and_reopen + set C2 [changeset_from_sql { + INSERT INTO t3 VALUES(1000, 11, 12); + DELETE FROM t4 WHERE x='g'; + }] + + faultsim_restore_and_reopen + sqlite3changeset_apply db $C1 [list xConflict OMIT] + faultsim_save_and_close +} {} + +do_faultsim_test 2.2 -faults oom* -prep { + catch {db2 close} + catch {db close} + faultsim_restore_and_reopen + sqlite3 db2 test.db2 +} -body { + set rebase [sqlite3changeset_apply_v2 db $::C2 [list xConflict OMIT]] + set {} {} +} -test { + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} +} +do_faultsim_test 2.3 -faults oom* -prep { + catch {db2 close} + catch {db close} + faultsim_restore_and_reopen + sqlite3 db2 test.db2 +} -body { + set rebase [sqlite3changeset_apply_v2 db $::C2 [list xConflict REPLACE]] + set {} {} +} -test { + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} +} +do_faultsim_test 2.4 -faults oom* -prep { + catch {db2 close} + catch {db close} + faultsim_restore_and_reopen + set ::rebase [sqlite3changeset_apply_v2 db $::C2 [list xConflict REPLACE]] +} -body { + sqlite3rebaser_create R + R configure $::rebase + R rebase $::C1 + set {} {} +} -test { + catch { R delete } + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} +} +do_faultsim_test 2.5 -faults oom* -prep { + catch {db2 close} + catch {db close} + faultsim_restore_and_reopen + set ::rebase [sqlite3changeset_apply_v2 db $::C2 [list xConflict OMIT]] +} -body { + sqlite3rebaser_create R + R configure $::rebase + R rebase $::C1 + set {} {} +} -test { + catch { R delete } + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} +} + +} + +reset_db +do_execsql_test 3.0 { + CREATE TABLE t1(x PRIMARY KEY, y, z); + INSERT INTO t1 VALUES(3, 1, 4); + INSERT INTO t1 VALUES(1, 5, 9); +} +faultsim_save_and_close + +proc xConflict {ret args} { return $ret } + +do_test 3.1 { + faultsim_restore_and_reopen + + execsql { BEGIN; UPDATE t1 SET z=11; } + set C1 [changeset_from_sql { + UPDATE t1 SET z=10 WHERE x=1; + }] + execsql { ROLLBACK } + + execsql { BEGIN; UPDATE t1 SET z=11; } + set C2 [changeset_from_sql { + UPDATE t1 SET z=55 WHERE x=1; + }] + execsql { ROLLBACK } + + set ::rebase1 [sqlite3changeset_apply_v2 db $::C1 [list xConflict OMIT]] + set ::rebase2 [sqlite3changeset_apply_v2 db $::C2 [list xConflict OMIT]] + set {} {} + execsql { SELECT * FROM t1 } +} {3 1 4 1 5 9} + + +do_faultsim_test 3.2 -faults oom* -prep { + faultsim_restore_and_reopen +} -body { + sqlite3rebaser_create R + R configure $::rebase1 + R configure $::rebase2 + set {} {} +} -test { + catch { R delete } + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} +} + finish_test ADDED ext/session/sessionrebase.test Index: ext/session/sessionrebase.test ================================================================== --- /dev/null +++ ext/session/sessionrebase.test @@ -0,0 +1,477 @@ +# 2018 March 14 +# +# 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 sessionrebase + +set ::lConflict [list] +proc xConflict {args} { + set res [lindex $::lConflict 0] + set ::lConflict [lrange $::lConflict 1 end] + return $res +} + +#------------------------------------------------------------------------- +# The following test cases - 1.* - test that the rebase blobs output by +# sqlite3_changeset_apply_v2 look correct in some simple cases. The blob +# is itself a changeset, containing records determined as follows: +# +# * For each conflict resolved with REPLACE, the rebase blob contains +# a DELETE record. All fields other than the PK fields are undefined. +# +# * For each conflict resolved with OMIT, the rebase blob contains an +# INSERT record. For an INSERT or UPDATE operation, the indirect flag +# is clear and all updated fields are defined. For a DELETE operation, +# the indirect flag is set and all non-PK fields left undefined. +# +proc do_apply_v2_test {tn sql modsql conflict_handler res} { + + execsql BEGIN + sqlite3session S db main + S attach * + execsql $sql + set changeset [S changeset] + S delete + execsql ROLLBACK + + execsql BEGIN + execsql $modsql + set ::lConflict $conflict_handler + set blob [sqlite3changeset_apply_v2 db $changeset xConflict] + execsql ROLLBACK + + uplevel [list do_test $tn [list changeset_to_list $blob] [list {*}$res]] +} + + +set ::lConflict [list] +proc xConflict {args} { + set res [lindex $::lConflict 0] + set ::lConflict [lrange $::lConflict 1 end] + return $res +} + +# Take a copy of database test.db in file test.db2. Execute $sql1 +# against test.db and $sql2 against test.db2. Capture a changeset +# for each. Then send the test.db2 changeset to test.db and apply +# it with the conflict handlers in $conflict_handler. Patch the +# test.db changeset and then execute it against test.db2. Test that +# the two databases come out the same. +# +proc do_rebase_test {tn sql1 sql2 conflict_handler {testsql ""} {testres ""}} { + + for {set i 1} {$i <= 2} {incr i} { + forcedelete test.db2 test.db2-journal test.db2-wal + forcecopy test.db test.db2 + sqlite3 db2 test.db2 + + db eval BEGIN + + sqlite3session S1 db main + S1 attach * + execsql $sql1 db + set c1 [S1 changeset] + S1 delete + + if {$i==1} { + sqlite3session S2 db2 main + S2 attach * + execsql $sql2 db2 + set c2 [S2 changeset] + S2 delete + } else { + set c2 [list] + foreach sql [split $sql2 ";"] { + if {[string is space $sql]} continue + sqlite3session S2 db2 main + S2 attach * + execsql $sql db2 + lappend c2 [S2 changeset] + S2 delete + } + } + + set ::lConflict $conflict_handler + set rebase [list] + if {$i==1} { + lappend rebase [sqlite3changeset_apply_v2 db $c2 xConflict] + } else { + foreach c $c2 { +#puts "apply_v2: [changeset_to_list $c]" + lappend rebase [sqlite3changeset_apply_v2 db $c xConflict] + } + #puts "llength: [llength $rebase]" + } + #if {$tn=="2.1.4"} { puts [changeset_to_list $rebase] ; breakpoint } + #puts [changeset_to_list [lindex $rebase 0]] ; breakpoint + #puts [llength $rebase] + + sqlite3rebaser_create R + foreach r $rebase { +#puts [changeset_to_list $r] + R configure $r + } + set c1r [R rebase $c1] + R delete + #if {$tn=="2.1.4"} { puts [changeset_to_list $c1r] } + + sqlite3changeset_apply_v2 db2 $c1r xConflictAbort + + if {[string range $tn end end]!="*"} { + uplevel [list do_test $tn.$i.1 [list compare_db db db2] {}] + } + db2 close + + if {$testsql!=""} { + uplevel [list do_execsql_test $tn.$i.2 $testsql $testres] + } + + db eval ROLLBACK + } +} + +do_execsql_test 1.0 { + CREATE TABLE t1(a INTEGER PRIMARY KEY, b); + INSERT INTO t1 VALUES(1, 'value A'); +} + +do_apply_v2_test 1.1.1 { + UPDATE t1 SET b = 'value B' WHERE a=1; +} { + UPDATE t1 SET b = 'value C' WHERE a=1; +} { + OMIT +} { + {INSERT t1 0 X. {} {i 1 t {value B}}} +} + +do_apply_v2_test 1.1.2 { + UPDATE t1 SET b = 'value B' WHERE a=1; +} { + UPDATE t1 SET b = 'value C' WHERE a=1; +} { + REPLACE +} { + {INSERT t1 1 X. {} {i 1 t {value B}}} +} + +do_apply_v2_test 1.2.1 { + INSERT INTO t1 VALUES(2, 'first'); +} { + INSERT INTO t1 VALUES(2, 'second'); +} { + OMIT +} { + {INSERT t1 0 X. {} {i 2 t first}} +} +do_apply_v2_test 1.2.2 { + INSERT INTO t1 VALUES(2, 'first'); +} { + INSERT INTO t1 VALUES(2, 'second'); +} { + REPLACE +} { + {INSERT t1 1 X. {} {i 2 t first}} +} + +do_apply_v2_test 1.3.1 { + DELETE FROM t1 WHERE a=1; +} { + UPDATE t1 SET b='value D' WHERE a=1; +} { + OMIT +} { + {DELETE t1 0 X. {i 1 t {value A}} {}} +} +do_apply_v2_test 1.3.2 { + DELETE FROM t1 WHERE a=1; +} { + UPDATE t1 SET b='value D' WHERE a=1; +} { + REPLACE +} { + {DELETE t1 1 X. {i 1 t {value A}} {}} +} + +#------------------------------------------------------------------------- +# Test cases 2.* - simple tests of rebasing actual changesets. +# +# 2.1.1 - 1u2u1r +# 2.1.2 - 1u2u2r +# 2.1.3 - 1d2d +# 2.1.4 - 1d2u1r +# 2.1.5 - 1d2u2r !! +# 2.1.6 - 1u2d1r +# 2.1.7 - 1u2d2r +# +# 2.1.8 - 1i2i2r +# 2.1.9 - 1i2i1r +# + +proc xConflictAbort {args} { + return "ABORT" +} + +reset_db +do_execsql_test 2.1.0 { + CREATE TABLE t1 (a INTEGER PRIMARY KEY, b TEXT); + INSERT INTO t1 VALUES(1, 'one'); + INSERT INTO t1 VALUES(2, 'two'); + INSERT INTO t1 VALUES(3, 'three'); +} +do_rebase_test 2.1.1 { + UPDATE t1 SET b = 'two.1' WHERE a=2 +} { + UPDATE t1 SET b = 'two.2' WHERE a=2; +} { + OMIT +} { SELECT * FROM t1 } {1 one 2 two.1 3 three} + +do_rebase_test 2.1.2 { + UPDATE t1 SET b = 'two.1' WHERE a=2 +} { + UPDATE t1 SET b = 'two.2' WHERE a=2; +} { + REPLACE +} { SELECT * FROM t1 } {1 one 2 two.2 3 three} + +do_rebase_test 2.1.3 { + DELETE FROM t1 WHERE a=3 +} { + DELETE FROM t1 WHERE a=3; +} { + OMIT +} { SELECT * FROM t1 } {1 one 2 two} + +do_rebase_test 2.1.4 { + DELETE FROM t1 WHERE a=1 +} { + UPDATE t1 SET b='one.2' WHERE a=1 +} { + OMIT +} { SELECT * FROM t1 } {2 two 3 three} + +#do_rebase_test 2.1.5 { +# DELETE FROM t1 WHERE a=1; +#} { +# UPDATE t1 SET b='one.2' WHERE a=1 +#} { +# REPLACE +#} { SELECT * FROM t1 } {2 two 3 three} + +do_rebase_test 2.1.6 { + UPDATE t1 SET b='three.1' WHERE a=3 +} { + DELETE FROM t1 WHERE a=3; +} { + OMIT +} { SELECT * FROM t1 } {1 one 2 two 3 three.1} + +do_rebase_test 2.1.7 { + UPDATE t1 SET b='three.1' WHERE a=3 +} { + DELETE FROM t1 WHERE a=3; +} { + REPLACE +} { SELECT * FROM t1 } {1 one 2 two} + +do_rebase_test 2.1.8 { + INSERT INTO t1 VALUES(4, 'four.1') +} { + INSERT INTO t1 VALUES(4, 'four.2'); +} { + REPLACE +} { SELECT * FROM t1 } {1 one 2 two 3 three 4 four.2} + +do_rebase_test 2.1.9 { + INSERT INTO t1 VALUES(4, 'four.1') +} { + INSERT INTO t1 VALUES(4, 'four.2'); +} { + OMIT +} { SELECT * FROM t1 } {1 one 2 two 3 three 4 four.1} + +do_execsql_test 2.2.0 { + CREATE TABLE t2(x, y, z PRIMARY KEY); + INSERT INTO t2 VALUES('i', 'a', 'A'); + INSERT INTO t2 VALUES('ii', 'b', 'B'); + INSERT INTO t2 VALUES('iii', 'c', 'C'); + + CREATE TABLE t3(a INTEGER PRIMARY KEY, b, c); + INSERT INTO t3 VALUES(-1, 'z', 'Z'); + INSERT INTO t3 VALUES(-2, 'y', 'Y'); +} + +do_rebase_test 2.2.1 { + UPDATE t2 SET x=1 WHERE z='A' +} { + UPDATE t2 SET y='one' WHERE z='A'; +} { +} { SELECT * FROM t2 WHERE z='A' } { 1 one A } + +do_rebase_test 2.2.2 { + UPDATE t2 SET x=1, y='one' WHERE z='B' +} { + UPDATE t2 SET y='two' WHERE z='B'; +} { + REPLACE +} { SELECT * FROM t2 WHERE z='B' } { 1 two B } + +do_rebase_test 2.2.3 { + UPDATE t2 SET x=1, y='one' WHERE z='B' +} { + UPDATE t2 SET y='two' WHERE z='B'; +} { + OMIT +} { SELECT * FROM t2 WHERE z='B' } { 1 one B } + +#------------------------------------------------------------------------- +reset_db +do_execsql_test 3.0 { + CREATE TABLE t3(a, b, c, PRIMARY KEY(b, c)); + CREATE TABLE abcdefghijkl(x PRIMARY KEY, y, z); + + INSERT INTO t3 VALUES(1, 2, 3); + INSERT INTO t3 VALUES(4, 2, 5); + INSERT INTO t3 VALUES(7, 2, 9); + + INSERT INTO abcdefghijkl VALUES('a', 'b', 'c'); + INSERT INTO abcdefghijkl VALUES('d', 'e', 'f'); + INSERT INTO abcdefghijkl VALUES('g', 'h', 'i'); +} + +breakpoint +# do_rebase_test 3.6.tn { +# UPDATE abcdefghijkl SET z='X', y='X' WHERE x='d'; +# } { +# UPDATE abcdefghijkl SET y=1 WHERE x='d'; +# UPDATE abcdefghijkl SET z=1 WHERE x='d'; +# } [list REPLACE REPLACE REPLACE] + +foreach {tn p} { + 1 OMIT 2 REPLACE +} { + do_rebase_test 3.1.$tn { + INSERT INTO t3 VALUES(1, 1, 1); + UPDATE abcdefghijkl SET y=2; + } { + INSERT INTO t3 VALUES(4, 1, 1); + DELETE FROM abcdefghijkl; + } [list $p $p $p $p $p $p $p $p] + + do_rebase_test 3.2.$tn { + INSERT INTO abcdefghijkl SELECT * FROM t3; + UPDATE t3 SET b=b+1; + } { + INSERT INTO t3 VALUES(3, 3, 3); + INSERT INTO abcdefghijkl SELECT * FROM t3; + } [list $p $p $p $p $p $p $p $p] + + do_rebase_test 3.3.$tn { + INSERT INTO abcdefghijkl VALUES(22, 23, 24); + } { + INSERT INTO abcdefghijkl VALUES(22, 25, 26); + UPDATE abcdefghijkl SET y=400 WHERE x=22; + } [list $p $p $p $p $p $p $p $p] + + do_rebase_test 3.4.$tn { + INSERT INTO abcdefghijkl VALUES(22, 23, 24); + } { + INSERT INTO abcdefghijkl VALUES(22, 25, 26); + UPDATE abcdefghijkl SET y=400 WHERE x=22; + } [list REPLACE $p] + + do_rebase_test 3.5.$tn* { + UPDATE abcdefghijkl SET y='X' WHERE x='d'; + } { + DELETE FROM abcdefghijkl WHERE x='d'; + INSERT INTO abcdefghijkl VALUES('d', NULL, NULL); + } [list $p $p $p] + do_rebase_test 3.5.$tn { + UPDATE abcdefghijkl SET y='X' WHERE x='d'; + } { + DELETE FROM abcdefghijkl WHERE x='d'; + INSERT INTO abcdefghijkl VALUES('d', NULL, NULL); + } [list REPLACE $p $p] + + do_rebase_test 3.6.$tn { + UPDATE abcdefghijkl SET z='X', y='X' WHERE x='d'; + } { + UPDATE abcdefghijkl SET y=1 WHERE x='d'; + UPDATE abcdefghijkl SET z=1 WHERE x='d'; + } [list REPLACE $p $p] +} + +#------------------------------------------------------------------------- +# Check that apply_v2() does not create a rebase buffer for a patchset. +# And that it is not possible to rebase a patchset. +# +do_execsql_test 4.0 { + CREATE TABLE t5(o PRIMARY KEY, p, q); + INSERT INTO t5 VALUES(1, 2, 3); + INSERT INTO t5 VALUES(4, 5, 6); +} +foreach {tn cmd rebasable} { + 1 patchset 0 + 2 changeset 1 +} { + proc xConflict {args} { return "OMIT" } + do_test 4.1.$tn { + execsql { + BEGIN; + DELETE FROM t5 WHERE o=4; + } + + sqlite3session S db main + S attach * + execsql { + INSERT INTO t5 VALUES(4, 'five', 'six'); + } + set P [S $cmd] + S delete + + execsql ROLLBACK; + + set ::rebase [sqlite3changeset_apply_v2 db $P xConflict] + expr [llength $::rebase]>0 + } $rebasable +} + +foreach {tn cmd rebasable} { + 1 patchset 0 + 2 changeset 1 +} { + do_test 4.2.$tn { + sqlite3session S db main + S attach * + execsql { + INSERT INTO t5 VALUES(5+$tn, 'five', 'six'); + } + set P [S $cmd] + S delete + + sqlite3rebaser_create R + R configure $::rebase + expr [catch {R rebase $P}]==0 + } $rebasable + + catch { R delete } +} +finish_test + Index: ext/session/sqlite3session.c ================================================================== --- ext/session/sqlite3session.c +++ ext/session/sqlite3session.c @@ -230,12 +230,12 @@ ** of a patchset change is associated with the correspondingly positioned ** table column, counting from left to right within the CREATE TABLE ** statement. ** ** For a DELETE change, all fields within the record except those associated -** with PRIMARY KEY columns are set to "undefined". The PRIMARY KEY fields -** contain the values identifying the row to delete. +** with PRIMARY KEY columns are omitted. The PRIMARY KEY fields contain the +** values identifying the row to delete. ** ** For an UPDATE change, all fields except those associated with PRIMARY KEY ** columns and columns that are modified by the UPDATE are set to "undefined". ** PRIMARY KEY fields contain the values identifying the table row to update, ** and fields associated with modified columns contain the new column values. @@ -514,11 +514,11 @@ ** the type byte). */ static int sessionSerialLen(u8 *a){ int e = *a; int n; - if( e==0 ) return 1; + if( e==0 || e==0xFF ) return 1; if( e==SQLITE_NULL ) return 1; if( e==SQLITE_INTEGER || e==SQLITE_FLOAT ) return 9; return sessionVarintGet(&a[1], &n) + 1 + n; } @@ -594,11 +594,11 @@ for(iCol=0; iColnCol; iCol++){ if( pTab->abPK[iCol] ){ int n1 = sessionSerialLen(a1); int n2 = sessionSerialLen(a2); - if( pTab->abPK[iCol] && (n1!=n2 || memcmp(a1, a2, n1)) ){ + if( n1!=n2 || memcmp(a1, a2, n1) ){ return 0; } a1 += n1; a2 += n2; }else{ @@ -2181,10 +2181,11 @@ if( 0==sqlite3_stricmp("sqlite_stat1", zTab) ){ zSql = sqlite3_mprintf( "SELECT tbl, ?2, stat FROM %Q.sqlite_stat1 WHERE tbl IS ?1 AND " "idx IS (CASE WHEN ?2=X'' THEN NULL ELSE ?2 END)", zDb ); + if( zSql==0 ) rc = SQLITE_NOMEM; }else{ int i; const char *zSep = ""; SessionBuffer buf = {0, 0, 0}; @@ -2916,11 +2917,12 @@ ** changes in the changeset. */ static int sessionChangesetNext( sqlite3_changeset_iter *p, /* Changeset iterator */ u8 **paRec, /* If non-NULL, store record pointer here */ - int *pnRec /* If non-NULL, store size of record here */ + int *pnRec, /* If non-NULL, store size of record here */ + int *pbNew /* If non-NULL, true if new table */ ){ int i; u8 op; assert( (paRec==0 && pnRec==0) || (paRec && pnRec) ); @@ -2951,10 +2953,11 @@ sessionDiscardData(&p->in); p->in.iCurrent = p->in.iNext; op = p->in.aData[p->in.iNext++]; while( op=='T' || op=='P' ){ + if( pbNew ) *pbNew = 1; p->bPatchset = (op=='P'); if( sessionChangesetReadTblhdr(p) ) return p->rc; if( (p->rc = sessionInputBuffer(&p->in, 2)) ) return p->rc; p->in.iCurrent = p->in.iNext; if( p->in.iNext>=p->in.nData ) return SQLITE_DONE; @@ -3029,11 +3032,11 @@ ** ** This function may not be called on iterators passed to a conflict handler ** callback by changeset_apply(). */ int sqlite3changeset_next(sqlite3_changeset_iter *p){ - return sessionChangesetNext(p, 0, 0); + return sessionChangesetNext(p, 0, 0, 0); } /* ** The following function extracts information on the current change ** from a changeset iterator. It may only be called after changeset_next() @@ -3408,10 +3411,12 @@ const char **azCol; /* Array of column names */ u8 *abPK; /* Boolean array - true if column is in PK */ int bStat1; /* True if table is sqlite_stat1 */ int bDeferConstraints; /* True to defer constraints */ SessionBuffer constraints; /* Deferred constraints are stored here */ + SessionBuffer rebase; /* Rebase information (if any) here */ + int bRebaseStarted; /* If table header is already in rebase */ }; /* ** Formulate a statement to DELETE a row from database db. Assuming a table ** structure like this: @@ -3674,11 +3679,10 @@ "DELETE FROM main.sqlite_stat1 WHERE tbl=?1 AND idx IS " "CASE WHEN length(?2)=0 AND typeof(?2)='blob' THEN NULL ELSE ?2 END " "AND (?4 OR stat IS ?3)" ); } - assert( rc==SQLITE_OK ); return rc; } /* ** A wrapper around sqlite3_bind_value() that detects an extra problem. @@ -3788,10 +3792,58 @@ if( rc!=SQLITE_ROW ) rc = sqlite3_reset(pSelect); } return rc; } + +/* +** This function is called from within sqlite3changset_apply_v2() when +** a conflict is encountered and resolved using conflict resolution +** mode eType (either SQLITE_CHANGESET_OMIT or SQLITE_CHANGESET_REPLACE).. +** It adds a conflict resolution record to the buffer in +** SessionApplyCtx.rebase, which will eventually be returned to the caller +** of apply_v2() as the "rebase" buffer. +** +** Return SQLITE_OK if successful, or an SQLite error code otherwise. +*/ +static int sessionRebaseAdd( + SessionApplyCtx *p, /* Apply context */ + int eType, /* Conflict resolution (OMIT or REPLACE) */ + sqlite3_changeset_iter *pIter /* Iterator pointing at current change */ +){ + int rc = SQLITE_OK; + int i; + int eOp = pIter->op; + if( p->bRebaseStarted==0 ){ + /* Append a table-header to the rebase buffer */ + const char *zTab = pIter->zTab; + sessionAppendByte(&p->rebase, 'T', &rc); + sessionAppendVarint(&p->rebase, p->nCol, &rc); + sessionAppendBlob(&p->rebase, p->abPK, p->nCol, &rc); + sessionAppendBlob(&p->rebase, (u8*)zTab, (int)strlen(zTab)+1, &rc); + p->bRebaseStarted = 1; + } + + assert( eType==SQLITE_CHANGESET_REPLACE||eType==SQLITE_CHANGESET_OMIT ); + assert( eOp==SQLITE_DELETE || eOp==SQLITE_INSERT || eOp==SQLITE_UPDATE ); + + sessionAppendByte(&p->rebase, + (eOp==SQLITE_DELETE ? SQLITE_DELETE : SQLITE_INSERT), &rc + ); + sessionAppendByte(&p->rebase, (eType==SQLITE_CHANGESET_REPLACE), &rc); + for(i=0; inCol; i++){ + sqlite3_value *pVal = 0; + if( eOp==SQLITE_DELETE || (eOp==SQLITE_UPDATE && p->abPK[i]) ){ + sqlite3changeset_old(pIter, i, &pVal); + }else{ + sqlite3changeset_new(pIter, i, &pVal); + } + sessionAppendValue(&p->rebase, pVal, &rc); + } + + return rc; +} /* ** Invoke the conflict handler for the change that the changeset iterator ** currently points to. ** @@ -3864,11 +3916,11 @@ /* Instead of invoking the conflict handler, append the change blob ** to the SessionApplyCtx.constraints buffer. */ u8 *aBlob = &pIter->in.aData[pIter->in.iCurrent]; int nBlob = pIter->in.iNext - pIter->in.iCurrent; sessionAppendBlob(&p->constraints, aBlob, nBlob, &rc); - res = SQLITE_CHANGESET_OMIT; + return SQLITE_OK; }else{ /* No other row with the new.* primary key. */ res = xConflict(pCtx, eType+1, pIter); if( res==SQLITE_CHANGESET_REPLACE ) rc = SQLITE_MISUSE; } @@ -3890,10 +3942,13 @@ default: rc = SQLITE_MISUSE; break; } + if( rc==SQLITE_OK ){ + rc = sessionRebaseAdd(p, res, pIter); + } } return rc; } @@ -4065,46 +4120,46 @@ int bReplace = 0; int bRetry = 0; int rc; rc = sessionApplyOneOp(pIter, pApply, xConflict, pCtx, &bReplace, &bRetry); - assert( rc==SQLITE_OK || (bRetry==0 && bReplace==0) ); - - /* If the bRetry flag is set, the change has not been applied due to an - ** SQLITE_CHANGESET_DATA problem (i.e. this is an UPDATE or DELETE and - ** a row with the correct PK is present in the db, but one or more other - ** fields do not contain the expected values) and the conflict handler - ** returned SQLITE_CHANGESET_REPLACE. In this case retry the operation, - ** but pass NULL as the final argument so that sessionApplyOneOp() ignores - ** the SQLITE_CHANGESET_DATA problem. */ - if( bRetry ){ - assert( pIter->op==SQLITE_UPDATE || pIter->op==SQLITE_DELETE ); - rc = sessionApplyOneOp(pIter, pApply, xConflict, pCtx, 0, 0); - } - - /* If the bReplace flag is set, the change is an INSERT that has not - ** been performed because the database already contains a row with the - ** specified primary key and the conflict handler returned - ** SQLITE_CHANGESET_REPLACE. In this case remove the conflicting row - ** before reattempting the INSERT. */ - else if( bReplace ){ - assert( pIter->op==SQLITE_INSERT ); - rc = sqlite3_exec(db, "SAVEPOINT replace_op", 0, 0, 0); - if( rc==SQLITE_OK ){ - rc = sessionBindRow(pIter, - sqlite3changeset_new, 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 ){ - rc = sessionApplyOneOp(pIter, pApply, xConflict, pCtx, 0, 0); - } - if( rc==SQLITE_OK ){ - rc = sqlite3_exec(db, "RELEASE replace_op", 0, 0, 0); + if( rc==SQLITE_OK ){ + /* If the bRetry flag is set, the change has not been applied due to an + ** SQLITE_CHANGESET_DATA problem (i.e. this is an UPDATE or DELETE and + ** a row with the correct PK is present in the db, but one or more other + ** fields do not contain the expected values) and the conflict handler + ** returned SQLITE_CHANGESET_REPLACE. In this case retry the operation, + ** but pass NULL as the final argument so that sessionApplyOneOp() ignores + ** the SQLITE_CHANGESET_DATA problem. */ + if( bRetry ){ + assert( pIter->op==SQLITE_UPDATE || pIter->op==SQLITE_DELETE ); + rc = sessionApplyOneOp(pIter, pApply, xConflict, pCtx, 0, 0); + } + + /* If the bReplace flag is set, the change is an INSERT that has not + ** been performed because the database already contains a row with the + ** specified primary key and the conflict handler returned + ** SQLITE_CHANGESET_REPLACE. In this case remove the conflicting row + ** before reattempting the INSERT. */ + else if( bReplace ){ + assert( pIter->op==SQLITE_INSERT ); + rc = sqlite3_exec(db, "SAVEPOINT replace_op", 0, 0, 0); + if( rc==SQLITE_OK ){ + rc = sessionBindRow(pIter, + sqlite3changeset_new, 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 ){ + rc = sessionApplyOneOp(pIter, pApply, xConflict, pCtx, 0, 0); + } + if( rc==SQLITE_OK ){ + rc = sqlite3_exec(db, "RELEASE replace_op", 0, 0, 0); + } } } return rc; } @@ -4176,11 +4231,12 @@ int(*xConflict)( void *pCtx, /* Copy of fifth arg to _apply() */ int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ sqlite3_changeset_iter *p /* Handle describing change and conflict */ ), - void *pCtx /* First argument passed to xConflict */ + void *pCtx, /* First argument passed to xConflict */ + void **ppRebase, int *pnRebase /* OUT: Rebase information */ ){ int schemaMismatch = 0; int rc; /* Return code */ const char *zTab = 0; /* Name of current table */ int nTab = 0; /* Result of sqlite3Strlen30(zTab) */ @@ -4214,13 +4270,22 @@ sqlite3_free((char*)sApply.azCol); /* cast works around VC++ bug */ sqlite3_finalize(sApply.pDelete); sqlite3_finalize(sApply.pUpdate); sqlite3_finalize(sApply.pInsert); sqlite3_finalize(sApply.pSelect); - memset(&sApply, 0, sizeof(sApply)); sApply.db = db; + sApply.pDelete = 0; + sApply.pUpdate = 0; + sApply.pInsert = 0; + sApply.pSelect = 0; + sApply.nCol = 0; + sApply.azCol = 0; + sApply.abPK = 0; + sApply.bStat1 = 0; sApply.bDeferConstraints = 1; + sApply.bRebaseStarted = 0; + memset(&sApply.constraints, 0, sizeof(SessionBuffer)); /* If an xFilter() callback was specified, invoke it now. If the ** xFilter callback returns zero, skip this table. If it returns ** non-zero, proceed. */ schemaMismatch = (xFilter && (0==xFilter(pCtx, zNew))); @@ -4326,19 +4391,55 @@ }else{ sqlite3_exec(db, "ROLLBACK TO changeset_apply", 0, 0, 0); sqlite3_exec(db, "RELEASE changeset_apply", 0, 0, 0); } + if( rc==SQLITE_OK && bPatchset==0 && ppRebase && pnRebase ){ + *ppRebase = (void*)sApply.rebase.aBuf; + *pnRebase = sApply.rebase.nBuf; + sApply.rebase.aBuf = 0; + } sqlite3_finalize(sApply.pInsert); sqlite3_finalize(sApply.pDelete); sqlite3_finalize(sApply.pUpdate); sqlite3_finalize(sApply.pSelect); sqlite3_free((char*)sApply.azCol); /* cast works around VC++ bug */ sqlite3_free((char*)sApply.constraints.aBuf); + sqlite3_free((char*)sApply.rebase.aBuf); sqlite3_mutex_leave(sqlite3_db_mutex(db)); return rc; } + +/* +** Apply the changeset passed via pChangeset/nChangeset to the main +** database attached to handle "db". +*/ +int sqlite3changeset_apply_v2( + sqlite3 *db, /* Apply change to "main" db of this handle */ + int nChangeset, /* Size of changeset in bytes */ + void *pChangeset, /* Changeset blob */ + int(*xFilter)( + void *pCtx, /* Copy of sixth arg to _apply() */ + const char *zTab /* Table name */ + ), + int(*xConflict)( + void *pCtx, /* Copy of sixth arg to _apply() */ + int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ + sqlite3_changeset_iter *p /* Handle describing change and conflict */ + ), + void *pCtx, /* First argument passed to xConflict */ + void **ppRebase, int *pnRebase +){ + sqlite3_changeset_iter *pIter; /* Iterator to skip through changeset */ + int rc = sqlite3changeset_start(&pIter, nChangeset, pChangeset); + if( rc==SQLITE_OK ){ + rc = sessionChangesetApply( + db, pIter, xFilter, xConflict, pCtx, ppRebase, pnRebase + ); + } + return rc; +} /* ** Apply the changeset passed via pChangeset/nChangeset to the main database ** attached to handle "db". Invoke the supplied conflict handler callback ** to resolve any conflicts encountered while applying the change. @@ -4356,23 +4457,45 @@ int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ sqlite3_changeset_iter *p /* Handle describing change and conflict */ ), void *pCtx /* First argument passed to xConflict */ ){ - sqlite3_changeset_iter *pIter; /* Iterator to skip through changeset */ - int rc = sqlite3changeset_start(&pIter, nChangeset, pChangeset); - if( rc==SQLITE_OK ){ - rc = sessionChangesetApply(db, pIter, xFilter, xConflict, pCtx); - } - return rc; + return sqlite3changeset_apply_v2( + db, nChangeset, pChangeset, xFilter, xConflict, pCtx, 0, 0 + ); } /* ** Apply the changeset passed via xInput/pIn to the main database ** attached to handle "db". Invoke the supplied conflict handler callback ** to resolve any conflicts encountered while applying the change. */ +int sqlite3changeset_apply_v2_strm( + sqlite3 *db, /* Apply change to "main" db of this handle */ + int (*xInput)(void *pIn, void *pData, int *pnData), /* Input function */ + void *pIn, /* First arg for xInput */ + int(*xFilter)( + void *pCtx, /* Copy of sixth arg to _apply() */ + const char *zTab /* Table name */ + ), + int(*xConflict)( + void *pCtx, /* Copy of sixth arg to _apply() */ + int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ + sqlite3_changeset_iter *p /* Handle describing change and conflict */ + ), + void *pCtx, /* First argument passed to xConflict */ + void **ppRebase, int *pnRebase +){ + sqlite3_changeset_iter *pIter; /* Iterator to skip through changeset */ + int rc = sqlite3changeset_start_strm(&pIter, xInput, pIn); + if( rc==SQLITE_OK ){ + rc = sessionChangesetApply( + db, pIter, xFilter, xConflict, pCtx, ppRebase, pnRebase + ); + } + return rc; +} int sqlite3changeset_apply_strm( sqlite3 *db, /* Apply change to "main" db of this handle */ int (*xInput)(void *pIn, void *pData, int *pnData), /* Input function */ void *pIn, /* First arg for xInput */ int(*xFilter)( @@ -4384,16 +4507,13 @@ int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ sqlite3_changeset_iter *p /* Handle describing change and conflict */ ), void *pCtx /* First argument passed to xConflict */ ){ - sqlite3_changeset_iter *pIter; /* Iterator to skip through changeset */ - int rc = sqlite3changeset_start_strm(&pIter, xInput, pIn); - if( rc==SQLITE_OK ){ - rc = sessionChangesetApply(db, pIter, xFilter, xConflict, pCtx); - } - return rc; + return sqlite3changeset_apply_v2_strm( + db, xInput, pIn, xFilter, xConflict, pCtx, 0, 0 + ); } /* ** sqlite3_changegroup handle. */ @@ -4408,31 +4528,90 @@ ** part of an sqlite3changeset_concat() operation. A new change object is ** allocated and a pointer to it stored in *ppNew. */ static int sessionChangeMerge( SessionTable *pTab, /* Table structure */ + int bRebase, /* True for a rebase hash-table */ int bPatchset, /* True for patchsets */ SessionChange *pExist, /* Existing change */ int op2, /* Second change operation */ int bIndirect, /* True if second change is indirect */ u8 *aRec, /* Second change record */ int nRec, /* Number of bytes in aRec */ SessionChange **ppNew /* OUT: Merged change */ ){ SessionChange *pNew = 0; + int rc = SQLITE_OK; if( !pExist ){ pNew = (SessionChange *)sqlite3_malloc(sizeof(SessionChange) + nRec); if( !pNew ){ return SQLITE_NOMEM; } memset(pNew, 0, sizeof(SessionChange)); pNew->op = op2; pNew->bIndirect = bIndirect; - pNew->nRecord = nRec; pNew->aRecord = (u8*)&pNew[1]; - memcpy(pNew->aRecord, aRec, nRec); + if( bIndirect==0 || bRebase==0 ){ + pNew->nRecord = nRec; + memcpy(pNew->aRecord, aRec, nRec); + }else{ + int i; + u8 *pIn = aRec; + u8 *pOut = pNew->aRecord; + for(i=0; inCol; i++){ + int nIn = sessionSerialLen(pIn); + if( *pIn==0 ){ + *pOut++ = 0; + }else if( pTab->abPK[i]==0 ){ + *pOut++ = 0xFF; + }else{ + memcpy(pOut, pIn, nIn); + pOut += nIn; + } + pIn += nIn; + } + pNew->nRecord = pOut - pNew->aRecord; + } + }else if( bRebase ){ + if( pExist->op==SQLITE_DELETE && pExist->bIndirect ){ + *ppNew = pExist; + }else{ + int nByte = nRec + pExist->nRecord + sizeof(SessionChange); + pNew = (SessionChange*)sqlite3_malloc(nByte); + if( pNew==0 ){ + rc = SQLITE_NOMEM; + }else{ + int i; + u8 *a1 = pExist->aRecord; + u8 *a2 = aRec; + u8 *pOut; + + memset(pNew, 0, nByte); + pNew->bIndirect = bIndirect || pExist->bIndirect; + pNew->op = op2; + pOut = pNew->aRecord = (u8*)&pNew[1]; + + for(i=0; inCol; i++){ + int n1 = sessionSerialLen(a1); + int n2 = sessionSerialLen(a2); + if( *a1==0xFF || (pTab->abPK[i]==0 && bIndirect) ){ + *pOut++ = 0xFF; + }else if( *a2==0 ){ + memcpy(pOut, a1, n1); + pOut += n1; + }else{ + memcpy(pOut, a2, n2); + pOut += n2; + } + a1 += n1; + a2 += n2; + } + pNew->nRecord = pOut - pNew->aRecord; + } + sqlite3_free(pExist); + } }else{ int op1 = pExist->op; /* ** op1=INSERT, op2=INSERT -> Unsupported. Discard op2. @@ -4522,28 +4701,28 @@ sqlite3_free(pExist); } } *ppNew = pNew; - return SQLITE_OK; + return rc; } /* ** Add all changes in the changeset traversed by the iterator passed as ** the first argument to the changegroup hash tables. */ static int sessionChangesetToHash( sqlite3_changeset_iter *pIter, /* Iterator to read from */ - sqlite3_changegroup *pGrp /* Changegroup object to add changeset to */ + sqlite3_changegroup *pGrp, /* Changegroup object to add changeset to */ + int bRebase /* True if hash table is for rebasing */ ){ u8 *aRec; int nRec; int rc = SQLITE_OK; SessionTable *pTab = 0; - - while( SQLITE_ROW==sessionChangesetNext(pIter, &aRec, &nRec) ){ + while( SQLITE_ROW==sessionChangesetNext(pIter, &aRec, &nRec, 0) ){ const char *zNew; int nCol; int op; int iHash; int bIndirect; @@ -4619,11 +4798,11 @@ pTab->nEntry--; break; } } - rc = sessionChangeMerge(pTab, + rc = sessionChangeMerge(pTab, bRebase, pIter->bPatchset, pExist, op, bIndirect, aRec, nRec, &pChange ); if( rc ) break; if( pChange ){ pChange->pNext = pTab->apChange[iHash]; @@ -4727,11 +4906,11 @@ sqlite3_changeset_iter *pIter; /* Iterator opened on pData/nData */ int rc; /* Return code */ rc = sqlite3changeset_start(&pIter, nData, pData); if( rc==SQLITE_OK ){ - rc = sessionChangesetToHash(pIter, pGrp); + rc = sessionChangesetToHash(pIter, pGrp, 0); } sqlite3changeset_finalize(pIter); return rc; } @@ -4758,11 +4937,11 @@ sqlite3_changeset_iter *pIter; /* Iterator opened on pData/nData */ int rc; /* Return code */ rc = sqlite3changeset_start_strm(&pIter, xInput, pIn); if( rc==SQLITE_OK ){ - rc = sessionChangesetToHash(pIter, pGrp); + rc = sessionChangesetToHash(pIter, pGrp, 0); } sqlite3changeset_finalize(pIter); return rc; } @@ -4842,7 +5021,350 @@ } sqlite3changegroup_delete(pGrp); return rc; } + +/* +** Changeset rebaser handle. +*/ +struct sqlite3_rebaser { + sqlite3_changegroup grp; /* Hash table */ +}; + +/* +** Buffers a1 and a2 must both contain a sessions module record nCol +** fields in size. This function appends an nCol sessions module +** record to buffer pBuf that is a copy of a1, except that for +** each field that is undefined in a1[], swap in the field from a2[]. +*/ +static void sessionAppendRecordMerge( + SessionBuffer *pBuf, /* Buffer to append to */ + int nCol, /* Number of columns in each record */ + u8 *a1, int n1, /* Record 1 */ + u8 *a2, int n2, /* Record 2 */ + int *pRc /* IN/OUT: error code */ +){ + sessionBufferGrow(pBuf, n1+n2, pRc); + if( *pRc==SQLITE_OK ){ + int i; + u8 *pOut = &pBuf->aBuf[pBuf->nBuf]; + for(i=0; inBuf = pOut-pBuf->aBuf; + assert( pBuf->nBuf<=pBuf->nAlloc ); + } +} + +/* +** This function is called when rebasing a local UPDATE change against one +** or more remote UPDATE changes. The aRec/nRec buffer contains the current +** old.* and new.* records for the change. The rebase buffer (a single +** record) is in aChange/nChange. The rebased change is appended to buffer +** pBuf. +** +** Rebasing the UPDATE involves: +** +** * Removing any changes to fields for which the corresponding field +** in the rebase buffer is set to "replaced" (type 0xFF). If this +** means the UPDATE change updates no fields, nothing is appended +** to the output buffer. +** +** * For each field modified by the local change for which the +** corresponding field in the rebase buffer is not "undefined" (0x00) +** or "replaced" (0xFF), the old.* value is replaced by the value +** in the rebase buffer. +*/ +static void sessionAppendPartialUpdate( + SessionBuffer *pBuf, /* Append record here */ + sqlite3_changeset_iter *pIter, /* Iterator pointed at local change */ + u8 *aRec, int nRec, /* Local change */ + u8 *aChange, int nChange, /* Record to rebase against */ + int *pRc /* IN/OUT: Return Code */ +){ + sessionBufferGrow(pBuf, 2+nRec+nChange, pRc); + if( *pRc==SQLITE_OK ){ + int bData = 0; + u8 *pOut = &pBuf->aBuf[pBuf->nBuf]; + int i; + u8 *a1 = aRec; + u8 *a2 = aChange; + + *pOut++ = SQLITE_UPDATE; + *pOut++ = pIter->bIndirect; + for(i=0; inCol; i++){ + int n1 = sessionSerialLen(a1); + int n2 = sessionSerialLen(a2); + if( pIter->abPK[i] || a2[0]==0 ){ + if( !pIter->abPK[i] ) bData = 1; + memcpy(pOut, a1, n1); + pOut += n1; + }else if( a2[0]!=0xFF ){ + bData = 1; + memcpy(pOut, a2, n2); + pOut += n2; + }else{ + *pOut++ = '\0'; + } + a1 += n1; + a2 += n2; + } + if( bData ){ + a2 = aChange; + for(i=0; inCol; i++){ + int n1 = sessionSerialLen(a1); + int n2 = sessionSerialLen(a2); + if( pIter->abPK[i] || a2[0]!=0xFF ){ + memcpy(pOut, a1, n1); + pOut += n1; + }else{ + *pOut++ = '\0'; + } + a1 += n1; + a2 += n2; + } + pBuf->nBuf = (pOut - pBuf->aBuf); + } + } +} + +/* +** pIter is configured to iterate through a changeset. This function rebases +** that changeset according to the current configuration of the rebaser +** object passed as the first argument. If no error occurs and argument xOutput +** is not NULL, then the changeset is returned to the caller by invoking +** xOutput zero or more times and SQLITE_OK returned. Or, if xOutput is NULL, +** then (*ppOut) is set to point to a buffer containing the rebased changeset +** before this function returns. In this case (*pnOut) is set to the size of +** the buffer in bytes. It is the responsibility of the caller to eventually +** free the (*ppOut) buffer using sqlite3_free(). +** +** If an error occurs, an SQLite error code is returned. If ppOut and +** pnOut are not NULL, then the two output parameters are set to 0 before +** returning. +*/ +static int sessionRebase( + sqlite3_rebaser *p, /* Rebaser hash table */ + sqlite3_changeset_iter *pIter, /* Input data */ + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut, /* Context for xOutput callback */ + int *pnOut, /* OUT: Number of bytes in output changeset */ + void **ppOut /* OUT: Inverse of pChangeset */ +){ + int rc = SQLITE_OK; + u8 *aRec = 0; + int nRec = 0; + int bNew = 0; + SessionTable *pTab = 0; + SessionBuffer sOut = {0,0,0}; + + while( SQLITE_ROW==sessionChangesetNext(pIter, &aRec, &nRec, &bNew) ){ + SessionChange *pChange = 0; + int bDone = 0; + + if( bNew ){ + const char *zTab = pIter->zTab; + for(pTab=p->grp.pList; pTab; pTab=pTab->pNext){ + if( 0==sqlite3_stricmp(pTab->zName, zTab) ) break; + } + bNew = 0; + + /* A patchset may not be rebased */ + if( pIter->bPatchset ){ + rc = SQLITE_ERROR; + } + + /* Append a table header to the output for this new table */ + sessionAppendByte(&sOut, pIter->bPatchset ? 'P' : 'T', &rc); + sessionAppendVarint(&sOut, pIter->nCol, &rc); + sessionAppendBlob(&sOut, pIter->abPK, pIter->nCol, &rc); + sessionAppendBlob(&sOut, (u8*)pIter->zTab, strlen(pIter->zTab)+1, &rc); + } + + if( pTab && rc==SQLITE_OK ){ + int iHash = sessionChangeHash(pTab, 0, aRec, pTab->nChange); + + for(pChange=pTab->apChange[iHash]; pChange; pChange=pChange->pNext){ + if( sessionChangeEqual(pTab, 0, aRec, 0, pChange->aRecord) ){ + break; + } + } + } + + if( pChange ){ + assert( pChange->op==SQLITE_DELETE || pChange->op==SQLITE_INSERT ); + switch( pIter->op ){ + case SQLITE_INSERT: + if( pChange->op==SQLITE_INSERT ){ + bDone = 1; + if( pChange->bIndirect==0 ){ + sessionAppendByte(&sOut, SQLITE_UPDATE, &rc); + sessionAppendByte(&sOut, pIter->bIndirect, &rc); + sessionAppendBlob(&sOut, pChange->aRecord, pChange->nRecord, &rc); + sessionAppendBlob(&sOut, aRec, nRec, &rc); + } + } + break; + + case SQLITE_UPDATE: + bDone = 1; + if( pChange->op==SQLITE_DELETE ){ + if( pChange->bIndirect==0 ){ + u8 *pCsr = aRec; + sessionSkipRecord(&pCsr, pIter->nCol); + sessionAppendByte(&sOut, SQLITE_INSERT, &rc); + sessionAppendByte(&sOut, pIter->bIndirect, &rc); + sessionAppendRecordMerge(&sOut, pIter->nCol, + pCsr, nRec-(pCsr-aRec), + pChange->aRecord, pChange->nRecord, &rc + ); + } + }else{ + sessionAppendPartialUpdate(&sOut, pIter, + aRec, nRec, pChange->aRecord, pChange->nRecord, &rc + ); + } + break; + + default: + assert( pIter->op==SQLITE_DELETE ); + bDone = 1; + if( pChange->op==SQLITE_INSERT ){ + sessionAppendByte(&sOut, SQLITE_DELETE, &rc); + sessionAppendByte(&sOut, pIter->bIndirect, &rc); + sessionAppendRecordMerge(&sOut, pIter->nCol, + pChange->aRecord, pChange->nRecord, aRec, nRec, &rc + ); + } + break; + } + } + + if( bDone==0 ){ + sessionAppendByte(&sOut, pIter->op, &rc); + sessionAppendByte(&sOut, pIter->bIndirect, &rc); + sessionAppendBlob(&sOut, aRec, nRec, &rc); + } + if( rc==SQLITE_OK && xOutput && sOut.nBuf>SESSIONS_STRM_CHUNK_SIZE ){ + rc = xOutput(pOut, sOut.aBuf, sOut.nBuf); + sOut.nBuf = 0; + } + if( rc ) break; + } + + if( rc!=SQLITE_OK ){ + sqlite3_free(sOut.aBuf); + memset(&sOut, 0, sizeof(sOut)); + } + + if( rc==SQLITE_OK ){ + if( xOutput ){ + if( sOut.nBuf>0 ){ + rc = xOutput(pOut, sOut.aBuf, sOut.nBuf); + } + }else{ + *ppOut = (void*)sOut.aBuf; + *pnOut = sOut.nBuf; + sOut.aBuf = 0; + } + } + sqlite3_free(sOut.aBuf); + return rc; +} + +/* +** Create a new rebaser object. +*/ +int sqlite3rebaser_create(sqlite3_rebaser **ppNew){ + int rc = SQLITE_OK; + sqlite3_rebaser *pNew; + + pNew = sqlite3_malloc(sizeof(sqlite3_rebaser)); + if( pNew==0 ){ + rc = SQLITE_NOMEM; + }else{ + memset(pNew, 0, sizeof(sqlite3_rebaser)); + } + *ppNew = pNew; + return rc; +} + +/* +** Call this one or more times to configure a rebaser. +*/ +int sqlite3rebaser_configure( + sqlite3_rebaser *p, + int nRebase, const void *pRebase +){ + sqlite3_changeset_iter *pIter = 0; /* Iterator opened on pData/nData */ + int rc; /* Return code */ + rc = sqlite3changeset_start(&pIter, nRebase, (void*)pRebase); + if( rc==SQLITE_OK ){ + rc = sessionChangesetToHash(pIter, &p->grp, 1); + } + sqlite3changeset_finalize(pIter); + return rc; +} + +/* +** Rebase a changeset according to current rebaser configuration +*/ +int sqlite3rebaser_rebase( + sqlite3_rebaser *p, + int nIn, const void *pIn, + int *pnOut, void **ppOut +){ + sqlite3_changeset_iter *pIter = 0; /* Iterator to skip through input */ + int rc = sqlite3changeset_start(&pIter, nIn, (void*)pIn); + + if( rc==SQLITE_OK ){ + rc = sessionRebase(p, pIter, 0, 0, pnOut, ppOut); + sqlite3changeset_finalize(pIter); + } + + return rc; +} + +/* +** Rebase a changeset according to current rebaser configuration +*/ +int sqlite3rebaser_rebase_strm( + sqlite3_rebaser *p, + int (*xInput)(void *pIn, void *pData, int *pnData), + void *pIn, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut +){ + sqlite3_changeset_iter *pIter = 0; /* Iterator to skip through input */ + int rc = sqlite3changeset_start_strm(&pIter, xInput, pIn); + + if( rc==SQLITE_OK ){ + rc = sessionRebase(p, pIter, xOutput, pOut, 0, 0); + sqlite3changeset_finalize(pIter); + } + + return rc; +} + +/* +** Destroy a rebaser object +*/ +void sqlite3rebaser_delete(sqlite3_rebaser *p){ + if( p ){ + sessionDeleteTable(p->grp.pList); + sqlite3_free(p); + } +} #endif /* SQLITE_ENABLE_SESSION && SQLITE_ENABLE_PREUPDATE_HOOK */ Index: ext/session/sqlite3session.h ================================================================== --- ext/session/sqlite3session.h +++ ext/session/sqlite3session.h @@ -946,23 +946,22 @@ void sqlite3changegroup_delete(sqlite3_changegroup*); /* ** CAPI3REF: Apply A Changeset To A Database ** -** Apply a changeset to a database. This function attempts to update the -** "main" database attached to handle db with the changes found in the -** changeset passed via the second and third arguments. +** Apply a changeset or patchset to a database. These functions attempt to +** update the "main" database attached to handle db with the changes found in +** the changeset passed via the second and third arguments. ** -** The fourth argument (xFilter) passed to this function is the "filter +** The fourth argument (xFilter) passed to these functions is the "filter ** callback". If it is not NULL, then for each table affected by at least one ** change in the changeset, the filter callback is invoked with ** the table name as the second argument, and a copy of the context pointer -** passed as the sixth argument to this function as the first. If the "filter -** callback" returns zero, then no attempt is made to apply any changes to -** the table. Otherwise, if the return value is non-zero or the xFilter -** argument to this function is NULL, all changes related to the table are -** attempted. +** passed as the sixth argument as the first. If the "filter callback" +** returns zero, then no attempt is made to apply any changes to the table. +** Otherwise, if the return value is non-zero or the xFilter argument to +** is NULL, all changes related to the table are attempted. ** ** For each table that is not excluded by the filter callback, this function ** tests that the target database contains a compatible table. A table is ** considered compatible if all of the following are true: ** @@ -1003,11 +1002,11 @@ ** the documentation for the three ** [SQLITE_CHANGESET_OMIT|available return values] for details. ** **
**
DELETE Changes
-** For each DELETE change, this function checks if the target database +** For each DELETE change, the function checks if the target database ** contains a row with the same primary key value (or values) as the ** original row values stored in the changeset. If it does, and the values ** stored in all non-primary key columns also match the values stored in ** the changeset the row is deleted from the target database. ** @@ -1048,11 +1047,11 @@ ** This includes the case where the INSERT operation is re-attempted because ** an earlier call to the conflict handler function returned ** [SQLITE_CHANGESET_REPLACE]. ** **
UPDATE Changes
-** For each UPDATE change, this function checks if the target database +** For each UPDATE change, the function checks if the target database ** contains a row with the same primary key value (or values) as the ** original row values stored in the changeset. If it does, and the values ** stored in all modified non-primary key columns also match the values ** stored in the changeset the row is updated within the target database. ** @@ -1079,15 +1078,25 @@ ** It is safe to execute SQL statements, including those that write to the ** table that the callback related to, from within the xConflict callback. ** This can be used to further customize the applications conflict ** resolution strategy. ** -** All changes made by this function are enclosed in a savepoint transaction. +** All changes made by these functions are enclosed in a savepoint transaction. ** If any other error (aside from a constraint failure when attempting to ** write to the target database) occurs, then the savepoint transaction is ** rolled back, restoring the target database to its original state, and an ** SQLite error code returned. +** +** If the output parameters (ppRebase) and (pnRebase) are non-NULL and +** the input is a changeset (not a patchset), then sqlite3changeset_apply_v2() +** may set (*ppRebase) to point to a "rebase" that may be used with the +** sqlite3_rebaser APIs buffer before returning. In this case (*pnRebase) +** is set to the size of the buffer in bytes. It is the responsibility of the +** caller to eventually free any such buffer using sqlite3_free(). The buffer +** is only allocated and populated if one or more conflicts were encountered +** while applying the patchset. See comments surrounding the sqlite3_rebaser +** APIs for further details. */ int sqlite3changeset_apply( sqlite3 *db, /* Apply change to "main" db of this handle */ int nChangeset, /* Size of changeset in bytes */ void *pChangeset, /* Changeset blob */ @@ -1100,10 +1109,26 @@ int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ sqlite3_changeset_iter *p /* Handle describing change and conflict */ ), void *pCtx /* First argument passed to xConflict */ ); +int sqlite3changeset_apply_v2( + sqlite3 *db, /* Apply change to "main" db of this handle */ + int nChangeset, /* Size of changeset in bytes */ + void *pChangeset, /* Changeset blob */ + int(*xFilter)( + void *pCtx, /* Copy of sixth arg to _apply() */ + const char *zTab /* Table name */ + ), + int(*xConflict)( + void *pCtx, /* Copy of sixth arg to _apply() */ + int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ + sqlite3_changeset_iter *p /* Handle describing change and conflict */ + ), + void *pCtx, /* First argument passed to xConflict */ + void **ppRebase, int *pnRebase +); /* ** CAPI3REF: Constants Passed To The Conflict Handler ** ** Values that may be passed as the second argument to a conflict-handler. @@ -1197,10 +1222,160 @@ */ #define SQLITE_CHANGESET_OMIT 0 #define SQLITE_CHANGESET_REPLACE 1 #define SQLITE_CHANGESET_ABORT 2 +/* +** CAPI3REF: Rebasing changesets +** +** Suppose there is a site hosting a database in state S0. And that +** modifications are made that move that database to state S1 and a +** changeset recorded (the "local" changeset). Then, a changeset based +** on S0 is received from another site (the "remote" changeset) and +** applied to the database. The database is then in state +** (S1+"remote"), where the exact state depends on any conflict +** resolution decisions (OMIT or REPLACE) made while applying "remote". +** Rebasing a changeset is to update it to take those conflict +** resolution decisions into account, so that the same conflicts +** do not have to be resolved elsewhere in the network. +** +** For example, if both the local and remote changesets contain an +** INSERT of the same key on "CREATE TABLE t1(a PRIMARY KEY, b)": +** +** local: INSERT INTO t1 VALUES(1, 'v1'); +** remote: INSERT INTO t1 VALUES(1, 'v2'); +** +** and the conflict resolution is REPLACE, then the INSERT change is +** removed from the local changeset (it was overridden). Or, if the +** conflict resolution was "OMIT", then the local changeset is modified +** to instead contain: +** +** UPDATE t1 SET b = 'v2' WHERE a=1; +** +** Changes within the local changeset are rebased as follows: +** +**
+**
Local INSERT
+** This may only conflict with a remote INSERT. If the conflict +** resolution was OMIT, then add an UPDATE change to the rebased +** changeset. Or, if the conflict resolution was REPLACE, add +** nothing to the rebased changeset. +** +**
Local DELETE
+** This may conflict with a remote UPDATE or DELETE. In both cases the +** only possible resolution is OMIT. If the remote operation was a +** DELETE, then add no change to the rebased changeset. If the remote +** operation was an UPDATE, then the old.* fields of change are updated +** to reflect the new.* values in the UPDATE. +** +**
Local UPDATE
+** This may conflict with a remote UPDATE or DELETE. If it conflicts +** with a DELETE, and the conflict resolution was OMIT, then the update +** is changed into an INSERT. Any undefined values in the new.* record +** from the update change are filled in using the old.* values from +** the conflicting DELETE. Or, if the conflict resolution was REPLACE, +** the UPDATE change is simply omitted from the rebased changeset. +** +** If conflict is with a remote UPDATE and the resolution is OMIT, then +** the old.* values are rebased using the new.* values in the remote +** change. Or, if the resolution is REPLACE, then the change is copied +** into the rebased changeset with updates to columns also updated by +** the conflicting remote UPDATE removed. If this means no columns would +** be updated, the change is omitted. +**
+** +** A local change may be rebased against multiple remote changes +** simultaneously. If a single key is modified by multiple remote +** changesets, they are combined as follows before the local changeset +** is rebased: +** +**
    +**
  • If there has been one or more REPLACE resolutions on a +** key, it is rebased according to a REPLACE. +** +**
  • If there have been no REPLACE resolutions on a key, then +** the local changeset is rebased according to the most recent +** of the OMIT resolutions. +**
+** +** Note that conflict resolutions from multiple remote changesets are +** combined on a per-field basis, not per-row. This means that in the +** case of multiple remote UPDATE operations, some fields of a single +** local change may be rebased for REPLACE while others are rebased for +** OMIT. +** +** In order to rebase a local changeset, the remote changeset must first +** be applied to the local database using sqlite3changeset_apply_v2() and +** the buffer of rebase information captured. Then: +** +**
    +**
  1. An sqlite3_rebaser object is created by calling +** sqlite3rebaser_create(). +**
  2. The new object is configured with the rebase buffer obtained from +** sqlite3changeset_apply_v2() by calling sqlite3rebaser_configure(). +** If the local changeset is to be rebased against multiple remote +** changesets, then sqlite3rebaser_configure() should be called +** multiple times, in the same order that the multiple +** sqlite3changeset_apply_v2() calls were made. +**
  3. Each local changeset is rebased by calling sqlite3rebaser_rebase(). +**
  4. The sqlite3_rebaser object is deleted by calling +** sqlite3rebaser_delete(). +**
+*/ +typedef struct sqlite3_rebaser sqlite3_rebaser; + +/* +** CAPIREF: Create a changeset rebaser object. +** +** Allocate a new changeset rebaser object. If successful, set (*ppNew) to +** point to the new object and return SQLITE_OK. Otherwise, if an error +** occurs, return an SQLite error code (e.g. SQLITE_NOMEM) and set (*ppNew) +** to NULL. +*/ +int sqlite3rebaser_create(sqlite3_rebaser **ppNew); + +/* +** CAPIREF: Configure a changeset rebaser object. +** +** Configure the changeset rebaser object to rebase changesets according +** to the conflict resolutions described by buffer pRebase (size nRebase +** bytes), which must have been obtained from a previous call to +** sqlite3changeset_apply_v2(). +*/ +int sqlite3rebaser_configure( + sqlite3_rebaser*, + int nRebase, const void *pRebase +); + +/* +** CAPIREF: Rebase a changeset +** +** Argument pIn must point to a buffer containing a changeset nIn bytes +** in size. This function allocates and populates a buffer with a copy +** of the changeset rebased rebased according to the configuration of the +** rebaser object passed as the first argument. If successful, (*ppOut) +** is set to point to the new buffer containing the rebased changset and +** (*pnOut) to its size in bytes and SQLITE_OK returned. It is the +** responsibility of the caller to eventually free the new buffer using +** sqlite3_free(). Otherwise, if an error occurs, (*ppOut) and (*pnOut) +** are set to zero and an SQLite error code returned. +*/ +int sqlite3rebaser_rebase( + sqlite3_rebaser*, + int nIn, const void *pIn, + int *pnOut, void **ppOut +); + +/* +** CAPIREF: Delete a changeset rebaser object. +** +** Delete the changeset rebaser object and all associated resources. There +** should be one call to this function for each successful invocation +** of sqlite3rebaser_create(). +*/ +void sqlite3rebaser_delete(sqlite3_rebaser *p); + /* ** CAPI3REF: Streaming Versions of API functions. ** ** The six streaming API xxx_strm() functions serve similar purposes to the ** corresponding non-streaming API functions: @@ -1300,10 +1475,26 @@ void *pCtx, /* Copy of sixth arg to _apply() */ int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ sqlite3_changeset_iter *p /* Handle describing change and conflict */ ), void *pCtx /* First argument passed to xConflict */ +); +int sqlite3changeset_apply_v2_strm( + sqlite3 *db, /* Apply change to "main" db of this handle */ + int (*xInput)(void *pIn, void *pData, int *pnData), /* Input function */ + void *pIn, /* First arg for xInput */ + int(*xFilter)( + void *pCtx, /* Copy of sixth arg to _apply() */ + const char *zTab /* Table name */ + ), + int(*xConflict)( + void *pCtx, /* Copy of sixth arg to _apply() */ + int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */ + sqlite3_changeset_iter *p /* Handle describing change and conflict */ + ), + void *pCtx, /* First argument passed to xConflict */ + void **ppRebase, int *pnRebase ); int sqlite3changeset_concat_strm( int (*xInputA)(void *pIn, void *pData, int *pnData), void *pInA, int (*xInputB)(void *pIn, void *pData, int *pnData), @@ -1337,10 +1528,17 @@ void *pIn ); int sqlite3changegroup_output_strm(sqlite3_changegroup*, int (*xOutput)(void *pOut, const void *pData, int nData), void *pOut +); +int sqlite3rebaser_rebase_strm( + sqlite3_rebaser *pRebaser, + int (*xInput)(void *pIn, void *pData, int *pnData), + void *pIn, + int (*xOutput)(void *pOut, const void *pData, int nData), + void *pOut ); /* ** Make sure we can call this stuff from C++. Index: ext/session/test_session.c ================================================================== --- ext/session/test_session.c +++ ext/session/test_session.c @@ -11,10 +11,14 @@ # include "tcl.h" # ifndef SQLITE_TCLAPI # define SQLITE_TCLAPI # endif #endif + +#ifndef SQLITE_AMALGAMATION + typedef unsigned char u8; +#endif typedef struct TestSession TestSession; struct TestSession { sqlite3_session *pSession; Tcl_Interp *interp; @@ -709,14 +713,12 @@ *pnData = nRet; return SQLITE_OK; } -/* -** sqlite3changeset_apply DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT? -*/ -static int SQLITE_TCLAPI test_sqlite3changeset_apply( +static int SQLITE_TCLAPI testSqlite3changesetApply( + int bV2, void * clientData, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[] ){ @@ -725,10 +727,12 @@ int rc; /* Return code from changeset_invert() */ void *pChangeset; /* Buffer containing changeset */ int nChangeset; /* Size of buffer aChangeset in bytes */ TestConflictHandler ctx; TestStreamInput sStr; + void *pRebase = 0; + int nRebase = 0; memset(&sStr, 0, sizeof(sStr)); sStr.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR); if( objc!=4 && objc!=5 ){ @@ -746,27 +750,71 @@ ctx.pConflictScript = objv[3]; ctx.pFilterScript = objc==5 ? objv[4] : 0; ctx.interp = interp; if( sStr.nStream==0 ){ - rc = sqlite3changeset_apply(db, nChangeset, pChangeset, - (objc==5) ? test_filter_handler : 0, test_conflict_handler, (void *)&ctx - ); + if( bV2==0 ){ + rc = sqlite3changeset_apply(db, nChangeset, pChangeset, + (objc==5)?test_filter_handler:0, test_conflict_handler, (void *)&ctx + ); + }else{ + rc = sqlite3changeset_apply_v2(db, nChangeset, pChangeset, + (objc==5)?test_filter_handler:0, test_conflict_handler, (void *)&ctx, + &pRebase, &nRebase + ); + } }else{ sStr.aData = (unsigned char*)pChangeset; sStr.nData = nChangeset; - rc = sqlite3changeset_apply_strm(db, testStreamInput, (void*)&sStr, - (objc==5) ? test_filter_handler : 0, test_conflict_handler, (void *)&ctx - ); + if( bV2==0 ){ + rc = sqlite3changeset_apply_strm(db, testStreamInput, (void*)&sStr, + (objc==5) ? test_filter_handler : 0, + test_conflict_handler, (void *)&ctx + ); + }else{ + rc = sqlite3changeset_apply_v2_strm(db, testStreamInput, (void*)&sStr, + (objc==5) ? test_filter_handler : 0, + test_conflict_handler, (void *)&ctx, + &pRebase, &nRebase + ); + } } if( rc!=SQLITE_OK ){ return test_session_error(interp, rc, 0); + }else{ + Tcl_ResetResult(interp); + if( bV2 && pRebase ){ + Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(pRebase, nRebase)); + } } - Tcl_ResetResult(interp); + sqlite3_free(pRebase); return TCL_OK; } + +/* +** sqlite3changeset_apply DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT? +*/ +static int SQLITE_TCLAPI test_sqlite3changeset_apply( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + return testSqlite3changesetApply(0, clientData, interp, objc, objv); +} +/* +** sqlite3changeset_apply_v2 DB CHANGESET CONFLICT-SCRIPT ?FILTER-SCRIPT? +*/ +static int SQLITE_TCLAPI test_sqlite3changeset_apply_v2( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + return testSqlite3changesetApply(1, clientData, interp, objc, objv); +} /* ** sqlite3changeset_apply_replace_all DB CHANGESET */ static int SQLITE_TCLAPI test_sqlite3changeset_apply_replace_all( @@ -1016,10 +1064,129 @@ return test_session_error(interp, rc, 0); } return TCL_OK; } + +/* +** tclcmd: CMD configure REBASE-BLOB +** tclcmd: CMD rebase CHANGESET +** tclcmd: CMD delete +*/ +static int SQLITE_TCLAPI test_rebaser_cmd( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + struct RebaseSubcmd { + const char *zSub; + int nArg; + const char *zMsg; + int iSub; + } aSub[] = { + { "configure", 1, "REBASE-BLOB" }, /* 0 */ + { "delete", 0, "" }, /* 1 */ + { "rebase", 1, "CHANGESET" }, /* 2 */ + { 0 } + }; + + sqlite3_rebaser *p = (sqlite3_rebaser*)clientData; + int iSub; + int rc; + + if( objc<2 ){ + Tcl_WrongNumArgs(interp, 1, objv, "SUBCOMMAND ..."); + return TCL_ERROR; + } + rc = Tcl_GetIndexFromObjStruct(interp, + objv[1], aSub, sizeof(aSub[0]), "sub-command", 0, &iSub + ); + if( rc!=TCL_OK ) return rc; + if( objc!=2+aSub[iSub].nArg ){ + Tcl_WrongNumArgs(interp, 2, objv, aSub[iSub].zMsg); + return TCL_ERROR; + } + + assert( iSub==0 || iSub==1 || iSub==2 ); + assert( rc==SQLITE_OK ); + switch( iSub ){ + case 0: { /* configure */ + int nRebase = 0; + unsigned char *pRebase = Tcl_GetByteArrayFromObj(objv[2], &nRebase); + rc = sqlite3rebaser_configure(p, nRebase, pRebase); + break; + } + + case 1: /* delete */ + Tcl_DeleteCommand(interp, Tcl_GetString(objv[0])); + break; + + default: { /* rebase */ + TestStreamInput sStr; /* Input stream */ + TestSessionsBlob sOut; /* Output blob */ + + memset(&sStr, 0, sizeof(sStr)); + memset(&sOut, 0, sizeof(sOut)); + sStr.aData = Tcl_GetByteArrayFromObj(objv[2], &sStr.nData); + sStr.nStream = test_tcl_integer(interp, SESSION_STREAM_TCL_VAR); + + if( sStr.nStream ){ + rc = sqlite3rebaser_rebase_strm(p, + testStreamInput, (void*)&sStr, + testStreamOutput, (void*)&sOut + ); + }else{ + rc = sqlite3rebaser_rebase(p, sStr.nData, sStr.aData, &sOut.n, &sOut.p); + } + + if( rc==SQLITE_OK ){ + Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(sOut.p, sOut.n)); + } + sqlite3_free(sOut.p); + break; + } + } + + if( rc!=SQLITE_OK ){ + return test_session_error(interp, rc, 0); + } + return TCL_OK; +} + +static void SQLITE_TCLAPI test_rebaser_del(void *clientData){ + sqlite3_rebaser *p = (sqlite3_rebaser*)clientData; + sqlite3rebaser_delete(p); +} + +/* +** tclcmd: sqlite3rebaser_create NAME +*/ +static int SQLITE_TCLAPI test_sqlite3rebaser_create( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + int rc; + sqlite3_rebaser *pNew = 0; + if( objc!=2 ){ + Tcl_WrongNumArgs(interp, 1, objv, "NAME"); + return SQLITE_ERROR; + } + + rc = sqlite3rebaser_create(&pNew); + if( rc!=SQLITE_OK ){ + return test_session_error(interp, rc, 0); + } + + Tcl_CreateObjCommand(interp, Tcl_GetString(objv[1]), test_rebaser_cmd, + (ClientData)pNew, test_rebaser_del + ); + Tcl_SetObjResult(interp, objv[1]); + return TCL_OK; +} int TestSession_Init(Tcl_Interp *interp){ struct Cmd { const char *zCmd; Tcl_ObjCmdProc *xProc; @@ -1027,13 +1194,15 @@ { "sqlite3session", test_sqlite3session }, { "sqlite3session_foreach", test_sqlite3session_foreach }, { "sqlite3changeset_invert", test_sqlite3changeset_invert }, { "sqlite3changeset_concat", test_sqlite3changeset_concat }, { "sqlite3changeset_apply", test_sqlite3changeset_apply }, + { "sqlite3changeset_apply_v2", test_sqlite3changeset_apply_v2 }, { "sqlite3changeset_apply_replace_all", test_sqlite3changeset_apply_replace_all }, { "sql_exec_changeset", test_sql_exec_changeset }, + { "sqlite3rebaser_create", test_sqlite3rebaser_create }, }; int i; for(i=0; i