Allow sqlite3session_apply() to apply changesets to tables that have been
extended using ALTER TABLE ADD COLUMN. FossilOrigin-Name: b20ff81ff9c8af500ea96e0ba9d34524220a89f1
This commit is contained in:
@@ -63,7 +63,10 @@ do_test 1.2.1 {
|
||||
INSERT INTO t1 VALUES(7, 8);
|
||||
}
|
||||
set ::log
|
||||
} {SQLITE_SCHEMA {sqlite3changeset_apply(): table t1 has 3 columns, expected 2}}
|
||||
} {}
|
||||
do_test 1.2.2 {
|
||||
db2 eval { SELECT * FROM t1 }
|
||||
} {5 6 {} 7 8 {}}
|
||||
|
||||
do_test 1.3.0 {
|
||||
execsql {
|
||||
|
||||
@@ -33,16 +33,11 @@ proc do_changeset_invert_test {tn session res} {
|
||||
|
||||
|
||||
proc do_conflict_test {tn args} {
|
||||
proc xConflict {args} {
|
||||
lappend ::xConflict $args
|
||||
return ""
|
||||
}
|
||||
proc bgerror {args} { set ::background_error $args }
|
||||
|
||||
|
||||
set O(-tables) [list]
|
||||
set O(-sql) [list]
|
||||
set O(-conflicts) [list]
|
||||
set O(-policy) "OMIT"
|
||||
|
||||
array set V $args
|
||||
foreach key [array names V] {
|
||||
@@ -50,6 +45,12 @@ proc do_conflict_test {tn args} {
|
||||
}
|
||||
array set O $args
|
||||
|
||||
proc xConflict {args} [subst -nocommands {
|
||||
lappend ::xConflict [set args]
|
||||
return $O(-policy)
|
||||
}]
|
||||
proc bgerror {args} { set ::background_error $args }
|
||||
|
||||
sqlite3session S db main
|
||||
foreach t $O(-tables) { S attach $t }
|
||||
execsql $O(-sql)
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
# 2017 February 04
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
#***********************************************************************
|
||||
#
|
||||
# Tests for the sessions module. Specifically, that a changeset can
|
||||
# be applied after ALTER TABLE ADD COLUMN has been used to add
|
||||
# columns to tables.
|
||||
#
|
||||
|
||||
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 sessionat
|
||||
|
||||
db close
|
||||
sqlite3_shutdown
|
||||
test_sqlite3_log log
|
||||
proc log {code msg} { lappend ::log $code $msg }
|
||||
|
||||
proc reset_test {} {
|
||||
catch { db close }
|
||||
catch { db2 close }
|
||||
forcedelete test.db test.db2
|
||||
sqlite3 db test.db
|
||||
sqlite3 db2 test.db2
|
||||
}
|
||||
|
||||
|
||||
# Run all tests in this file twice. Once with "WITHOUT ROWID", and once
|
||||
# with regular rowid tables.
|
||||
#
|
||||
# ?.1.*: Test that PK inconsistencies are detected if one or more of the PK
|
||||
# columns are not present in the changeset.
|
||||
#
|
||||
# ?.2.*: Test that it is not possible to apply a changeset with N columns
|
||||
# to a db with fewer than N columns.
|
||||
#
|
||||
# ?.3.*: Test some INSERT, UPDATE and DELETE operations that do not
|
||||
# require conflict handling.
|
||||
#
|
||||
# ?.4.*: Test some INSERT, UPDATE and DELETE operations that do require
|
||||
# conflict handling.
|
||||
#
|
||||
# ?.5.*: Test that attempting to concat two changesets with different
|
||||
# numbers of columns for the same table is an error.
|
||||
#
|
||||
foreach {tn trailing} {
|
||||
sessionat-ipk ""
|
||||
sessionat-wor " WITHOUT ROWID "
|
||||
} {
|
||||
eval [string map [list %WR% $trailing] {
|
||||
reset_test
|
||||
|
||||
#-----------------------------------------------------------------------
|
||||
do_execsql_test $tn.1.0 {
|
||||
CREATE TABLE t1(a, b, PRIMARY KEY(a)) %WR%;
|
||||
}
|
||||
do_execsql_test -db db2 $tn.1.1 {
|
||||
CREATE TABLE t1(a, b, c, PRIMARY KEY(a, c)) %WR%;
|
||||
}
|
||||
do_test $tn.1.2 {
|
||||
set ::log {}
|
||||
do_then_apply_sql { INSERT INTO t1 VALUES('one', 'two') }
|
||||
set ::log
|
||||
} [list \
|
||||
SQLITE_SCHEMA {sqlite3changeset_apply(): primary key mismatch for table t1}
|
||||
]
|
||||
do_execsql_test $tn.1.3 { SELECT * FROM t1 } {one two}
|
||||
do_execsql_test -db db2 $tn.1.4 { SELECT * FROM t1 } {}
|
||||
|
||||
#-----------------------------------------------------------------------
|
||||
do_execsql_test $tn.2.0 {
|
||||
CREATE TABLE t2(x, y, z, PRIMARY KEY(x)) %WR%;
|
||||
}
|
||||
do_execsql_test -db db2 $tn.2.1 {
|
||||
CREATE TABLE t2(x, y, PRIMARY KEY(x)) %WR%;
|
||||
}
|
||||
do_test $tn.2.2 {
|
||||
db cache flush
|
||||
set ::log {}
|
||||
do_then_apply_sql { INSERT INTO t2 VALUES(1, 2, 3) }
|
||||
set ::log
|
||||
} [list SQLITE_SCHEMA \
|
||||
{sqlite3changeset_apply(): table t2 has 2 columns, expected 3 or more}
|
||||
]
|
||||
do_execsql_test $tn.2.3 { SELECT * FROM t2 } {1 2 3}
|
||||
do_execsql_test -db db2 $tn.2.4 { SELECT * FROM t2 } {}
|
||||
|
||||
#-----------------------------------------------------------------------
|
||||
do_execsql_test $tn.3.0 {
|
||||
CREATE TABLE t3(a, b, PRIMARY KEY(b)) %WR%;
|
||||
}
|
||||
do_execsql_test -db db2 $tn.3.1 {
|
||||
CREATE TABLE t3(a, b, c DEFAULT 'D', PRIMARY KEY(b)) %WR%;
|
||||
}
|
||||
do_test $tn.3.2 {
|
||||
do_then_apply_sql {
|
||||
INSERT INTO t3 VALUES(1, 2);
|
||||
INSERT INTO t3 VALUES(3, 4);
|
||||
INSERT INTO t3 VALUES(5, 6);
|
||||
};
|
||||
db2 eval {SELECT * FROM t3}
|
||||
} {1 2 D 3 4 D 5 6 D}
|
||||
do_test $tn.3.3 {
|
||||
do_then_apply_sql {
|
||||
UPDATE t3 SET a=45 WHERE b=4;
|
||||
DELETE FROM t3 WHERE a=5;
|
||||
};
|
||||
db2 eval {SELECT * FROM t3}
|
||||
} {1 2 D 45 4 D}
|
||||
|
||||
#-----------------------------------------------------------------------
|
||||
# 4.1: INSERT statements
|
||||
# 4.2: DELETE statements
|
||||
# 4.3: UPDATE statements
|
||||
#
|
||||
do_execsql_test $tn.4.1.0 {
|
||||
CREATE TABLE t4(x INTEGER PRIMARY KEY, y) %WR%;
|
||||
}
|
||||
do_execsql_test -db db2 $tn.4.1.1 {
|
||||
CREATE TABLE t4(x INTEGER PRIMARY KEY, y, z) %WR%;
|
||||
INSERT INTO t4 VALUES(1, 2, 3);
|
||||
INSERT INTO t4 VALUES(4, 5, 6);
|
||||
}
|
||||
do_conflict_test $tn.4.1.2 -tables t4 -sql {
|
||||
INSERT INTO t4 VALUES(10, 20);
|
||||
INSERT INTO t4 VALUES(4, 11);
|
||||
} -conflicts {
|
||||
{INSERT t4 CONFLICT {i 4 i 11} {i 4 i 5}}
|
||||
}
|
||||
do_execsql_test -db db2 $tn.4.1.3 {
|
||||
SELECT * FROM t4 ORDER BY x
|
||||
} {1 2 3 4 5 6 10 20 {}}
|
||||
do_conflict_test $tn.4.1.4 -policy REPLACE -tables t4 -sql {
|
||||
INSERT INTO t4 VALUES(1, 11);
|
||||
} -conflicts {
|
||||
{INSERT t4 CONFLICT {i 1 i 11} {i 1 i 2}}
|
||||
}
|
||||
do_execsql_test -db db2 $tn.4.1.5 {
|
||||
SELECT * FROM t4 ORDER BY x
|
||||
} {1 11 {} 4 5 6 10 20 {}}
|
||||
|
||||
do_execsql_test $tn.4.2.0 {
|
||||
DELETE FROM t4;
|
||||
INSERT INTO t4 VALUES(1, 'A');
|
||||
INSERT INTO t4 VALUES(2, 'B');
|
||||
INSERT INTO t4 VALUES(3, 'C');
|
||||
INSERT INTO t4 VALUES(4, 'D');
|
||||
}
|
||||
do_execsql_test -db db2 $tn.4.2.1 {
|
||||
DELETE FROM t4;
|
||||
INSERT INTO t4 VALUES(1, 'A', 'a');
|
||||
INSERT INTO t4 VALUES(3, 'C', 'c');
|
||||
INSERT INTO t4 VALUES(4, 'E', 'd');
|
||||
}
|
||||
do_conflict_test $tn.4.2.2 -tables t4 -sql {
|
||||
DELETE FROM t4 WHERE x=2;
|
||||
DELETE FROM t4 WHERE x=4;
|
||||
} -conflicts {
|
||||
{DELETE t4 NOTFOUND {i 2 t B}}
|
||||
{DELETE t4 DATA {i 4 t D} {i 4 t E}}
|
||||
}
|
||||
|
||||
do_execsql_test $tn.4.3.0 {
|
||||
CREATE TABLE t5(a, b, c PRIMARY KEY) %WR%;
|
||||
INSERT INTO t5 VALUES(1,1,1), (2,2,2), (3,3,3), (4,4,4);
|
||||
}
|
||||
do_execsql_test -db db2 $tn.4.3.1 {
|
||||
CREATE TABLE t5(a, b, c PRIMARY KEY, d CHECK(b!=10)) %WR%;
|
||||
INSERT INTO t5 VALUES (2,2,2,2), (3,8,3,3), (4,4,4,4);
|
||||
}
|
||||
do_conflict_test $tn.4.3.2 -tables t5 -sql {
|
||||
UPDATE t5 SET a=4 WHERE c=1;
|
||||
UPDATE t5 SET b=9 WHERE c=3;
|
||||
UPDATE t5 SET b=10 WHERE c=2;
|
||||
} -conflicts {
|
||||
{UPDATE t5 NOTFOUND {i 1 {} {} i 1} {i 4 {} {} {} {}}}
|
||||
{UPDATE t5 DATA {{} {} i 3 i 3} {{} {} i 9 {} {}} {i 3 i 8 i 3}}
|
||||
{UPDATE t5 CONSTRAINT {{} {} i 2 i 2} {{} {} i 10 {} {}}}
|
||||
}
|
||||
|
||||
#-----------------------------------------------------------------------
|
||||
do_execsql_test $tn.5.0 {
|
||||
CREATE TABLE t6(a, b, c, PRIMARY KEY(a, b)) %WR%;
|
||||
}
|
||||
do_execsql_test -db db2 $tn.5.1 {
|
||||
CREATE TABLE t6(a, b, c, d, e, PRIMARY KEY(a, b)) %WR%;
|
||||
}
|
||||
do_test $tn.5.2 {
|
||||
set c1 [sql_exec_changeset db {
|
||||
INSERT INTO t6 VALUES(1, 1, 1);
|
||||
INSERT INTO t6 VALUES(2, 2, 2);
|
||||
}]
|
||||
set c2 [sql_exec_changeset db2 {
|
||||
INSERT INTO t6 VALUES(3, 3, 3, 3, 3);
|
||||
INSERT INTO t6 VALUES(4, 4, 4, 4, 4);
|
||||
}]
|
||||
list [catch { sqlite3changeset_concat $c1 $c2} msg] $msg
|
||||
} {1 SQLITE_SCHEMA}
|
||||
|
||||
}]
|
||||
}
|
||||
|
||||
|
||||
finish_test
|
||||
@@ -3028,7 +3028,7 @@ int sqlite3changeset_conflict(
|
||||
if( !pIter->pConflict ){
|
||||
return SQLITE_MISUSE;
|
||||
}
|
||||
if( iVal<0 || iVal>=sqlite3_column_count(pIter->pConflict) ){
|
||||
if( iVal<0 || iVal>=pIter->nCol ){
|
||||
return SQLITE_RANGE;
|
||||
}
|
||||
*ppValue = sqlite3_column_value(pIter->pConflict, iVal);
|
||||
@@ -3495,7 +3495,13 @@ static int sessionInsertRow(
|
||||
|
||||
sessionAppendStr(&buf, "INSERT INTO main.", &rc);
|
||||
sessionAppendIdent(&buf, zTab, &rc);
|
||||
sessionAppendStr(&buf, " VALUES(?", &rc);
|
||||
sessionAppendStr(&buf, "(", &rc);
|
||||
for(i=0; i<p->nCol; i++){
|
||||
if( i!=0 ) sessionAppendStr(&buf, ", ", &rc);
|
||||
sessionAppendIdent(&buf, p->azCol[i], &rc);
|
||||
}
|
||||
|
||||
sessionAppendStr(&buf, ") VALUES(?", &rc);
|
||||
for(i=1; i<p->nCol; i++){
|
||||
sessionAppendStr(&buf, ", ?", &rc);
|
||||
}
|
||||
@@ -4041,11 +4047,17 @@ static int sessionChangesetApply(
|
||||
nTab = (int)strlen(zTab);
|
||||
sApply.azCol = (const char **)zTab;
|
||||
}else{
|
||||
int nMinCol = 0;
|
||||
int i;
|
||||
|
||||
sqlite3changeset_pk(pIter, &abPK, 0);
|
||||
rc = sessionTableInfo(
|
||||
db, "main", zNew, &sApply.nCol, &zTab, &sApply.azCol, &sApply.abPK
|
||||
);
|
||||
if( rc!=SQLITE_OK ) break;
|
||||
for(i=0; i<sApply.nCol; i++){
|
||||
if( sApply.abPK[i] ) nMinCol = i+1;
|
||||
}
|
||||
|
||||
if( sApply.nCol==0 ){
|
||||
schemaMismatch = 1;
|
||||
@@ -4053,26 +4065,29 @@ static int sessionChangesetApply(
|
||||
"sqlite3changeset_apply(): no such table: %s", zTab
|
||||
);
|
||||
}
|
||||
else if( sApply.nCol!=nCol ){
|
||||
else if( sApply.nCol<nCol ){
|
||||
schemaMismatch = 1;
|
||||
sqlite3_log(SQLITE_SCHEMA,
|
||||
"sqlite3changeset_apply(): table %s has %d columns, expected %d",
|
||||
"sqlite3changeset_apply(): table %s has %d columns, "
|
||||
"expected %d or more",
|
||||
zTab, sApply.nCol, nCol
|
||||
);
|
||||
}
|
||||
else if( memcmp(sApply.abPK, abPK, nCol)!=0 ){
|
||||
else if( nCol<nMinCol || memcmp(sApply.abPK, abPK, nCol)!=0 ){
|
||||
schemaMismatch = 1;
|
||||
sqlite3_log(SQLITE_SCHEMA, "sqlite3changeset_apply(): "
|
||||
"primary key mismatch for table %s", zTab
|
||||
);
|
||||
}
|
||||
else if(
|
||||
(rc = sessionSelectRow(db, zTab, &sApply))
|
||||
|| (rc = sessionUpdateRow(db, zTab, &sApply))
|
||||
|| (rc = sessionDeleteRow(db, zTab, &sApply))
|
||||
|| (rc = sessionInsertRow(db, zTab, &sApply))
|
||||
){
|
||||
break;
|
||||
else{
|
||||
sApply.nCol = nCol;
|
||||
if((rc = sessionSelectRow(db, zTab, &sApply))
|
||||
|| (rc = sessionUpdateRow(db, zTab, &sApply))
|
||||
|| (rc = sessionDeleteRow(db, zTab, &sApply))
|
||||
|| (rc = sessionInsertRow(db, zTab, &sApply))
|
||||
){
|
||||
break;
|
||||
}
|
||||
}
|
||||
nTab = sqlite3Strlen30(zTab);
|
||||
}
|
||||
|
||||
@@ -319,7 +319,8 @@ int sqlite3session_changeset(
|
||||
** the from-table, a DELETE record is added to the session object.
|
||||
**
|
||||
** <li> For each row (primary key) that exists in both tables, but features
|
||||
** different in each, an UPDATE record is added to the session.
|
||||
** different non-PK values in each, an UPDATE record is added to the
|
||||
** session.
|
||||
** </ul>
|
||||
**
|
||||
** To clarify, if this function is called and then a changeset constructed
|
||||
@@ -904,7 +905,7 @@ void sqlite3changegroup_delete(sqlite3_changegroup*);
|
||||
** <ul>
|
||||
** <li> The table has the same name as the name recorded in the
|
||||
** changeset, and
|
||||
** <li> The table has the same number of columns as recorded in the
|
||||
** <li> The table has at least as many columns as recorded in the
|
||||
** changeset, and
|
||||
** <li> The table has primary key columns in the same position as
|
||||
** recorded in the changeset.
|
||||
@@ -949,7 +950,11 @@ void sqlite3changegroup_delete(sqlite3_changegroup*);
|
||||
** If a row with matching primary key values is found, but one or more of
|
||||
** the non-primary key fields contains a value different from the original
|
||||
** row value stored in the changeset, the conflict-handler function is
|
||||
** invoked with [SQLITE_CHANGESET_DATA] as the second argument.
|
||||
** invoked with [SQLITE_CHANGESET_DATA] as the second argument. If the
|
||||
** database table has more columns than are recorded in the changeset,
|
||||
** only the values of those non-primary key fields are compared against
|
||||
** the current database contents - any trailing database table columns
|
||||
** are ignored.
|
||||
**
|
||||
** If no row with matching primary key values is found in the database,
|
||||
** the conflict-handler function is invoked with [SQLITE_CHANGESET_NOTFOUND]
|
||||
@@ -964,7 +969,9 @@ void sqlite3changegroup_delete(sqlite3_changegroup*);
|
||||
**
|
||||
** <dt>INSERT Changes<dd>
|
||||
** For each INSERT change, an attempt is made to insert the new row into
|
||||
** the database.
|
||||
** the database. If the changeset row contains fewer fields than the
|
||||
** database table, the trailing fields are populated with their default
|
||||
** values.
|
||||
**
|
||||
** If the attempt to insert the row fails because the database already
|
||||
** contains a row with the same primary key values, the conflict handler
|
||||
@@ -982,13 +989,13 @@ void sqlite3changegroup_delete(sqlite3_changegroup*);
|
||||
** For each UPDATE change, this 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 updated within the target database.
|
||||
** stored in all modified non-primary key columns also match the values
|
||||
** stored in the changeset the row is updated within the target database.
|
||||
**
|
||||
** If a row with matching primary key values is found, but one or more of
|
||||
** the non-primary key fields contains a value different from an original
|
||||
** row value stored in the changeset, the conflict-handler function is
|
||||
** invoked with [SQLITE_CHANGESET_DATA] as the second argument. Since
|
||||
** the modified non-primary key fields contains a value different from an
|
||||
** original row value stored in the changeset, the conflict-handler function
|
||||
** is invoked with [SQLITE_CHANGESET_DATA] as the second argument. Since
|
||||
** UPDATE changes only contain values for non-primary key fields that are
|
||||
** to be modified, only those fields need to match the original values to
|
||||
** avoid the SQLITE_CHANGESET_DATA conflict-handler callback.
|
||||
|
||||
Reference in New Issue
Block a user