reset_db
proc do_patchconcat_test {tn args} {
+ set bRevert 0
+ if {[lindex $args 0] == "-revert"} {
+ set bRevert 1
+ set args [lrange $args 1 end]
+ }
set nSql [expr [llength $args]-1]
set res [lindex $args $nSql]
set patchlist [list]
execsql BEGIN
+ if {$bRevert} { execsql { SAVEPOINT x } }
foreach sql [lrange $args 0 end-1] {
sqlite3session S db main
S attach *
execsql $sql
lappend patchlist [S patchset]
S delete
+ if {$bRevert} { execsql { ROLLBACK TO x } }
}
execsql ROLLBACK
{INSERT t1 0 X.. {} {i 4 i 5 i 6}}
}
-if 0 {
do_execsql_test 4.2.1 {
INSERT INTO t1 VALUES(1, 2, 3);
INSERT INTO t1 VALUES(4, 5, 6);
}
+
do_patchconcat_test 4.2.2 {
UPDATE t1 SET z = 'abc' WHERE x=1
} {
UPDATE t1 SET z = 'def' WHERE x=4
} {
+ {UPDATE t1 0 X.. {i 1 {} {} {} {}} {{} {} {} {} t abc}}
+ {UPDATE t1 0 X.. {i 4 {} {} {} {}} {{} {} {} {} t def}}
+}
+
+do_patchconcat_test 4.2.3 {
+ DELETE FROM t1 WHERE x=1;
+} {
+ DELETE FROM t1 WHERE x=4;
+} {
+ {DELETE t1 0 X.. {i 1 {} {} {} {}} {}}
+ {DELETE t1 0 X.. {i 4 {} {} {} {}} {}}
+}
+
+
+do_execsql_test 4.3.1 {
+ CREATE TABLE t2(a, b, c, d, PRIMARY KEY(c, b));
+ INSERT INTO t2 VALUES('.', 1, 1, '.');
+ INSERT INTO t2 VALUES('.', 1, 2, '.');
+ INSERT INTO t2 VALUES('.', 2, 1, '.');
+ INSERT INTO t2 VALUES('.', 2, 2, '.');
+}
+
+# INSERT + INSERT
+do_patchconcat_test 4.3.2 -revert {
+ INSERT INTO t2 VALUES('a', 'a', 'a', 'a');
+} {
+ INSERT INTO t2 VALUES('b', 'a', 'a', 'b');
+} {
+ {INSERT t2 0 .XX. {} {t a t a t a t a}}
+}
+
+# INSERT + DELETE
+do_patchconcat_test 4.3.3 {
+ INSERT INTO t2 VALUES('a', 'a', 'a', 'a');
+} {
+ DELETE FROM t2 WHERE c = 'a';
+} {
+}
+
+# INSERT + UPDATE
+do_patchconcat_test 4.3.4 {
+ INSERT INTO t2 VALUES('a', 'a', 'a', 'a');
+} {
+ UPDATE t2 SET d = 'b' WHERE c='a';
+} {
+ {INSERT t2 0 .XX. {} {t a t a t a t b}}
+}
+
+# UPDATE + UPDATE
+do_patchconcat_test 4.3.5 {
+ UPDATE t2 SET a = 'a' WHERE c=1 AND b=2;
+} {
+ UPDATE t2 SET d = 'd' WHERE c=1 AND b=2;
+} {
+ {UPDATE t2 0 .XX. {{} {} i 2 i 1 {} {}} {t a {} {} {} {} t d}}
+}
+
+# UPDATE + DELETE
+do_patchconcat_test 4.3.6 {
+ UPDATE t2 SET a = 'a' WHERE c=1 AND b=2;
+} {
+ DELETE FROM t2 WHERE c=1 AND b=2;
+} {
+ {DELETE t2 0 .XX. {{} {} i 2 i 1 {} {}} {}}
+}
+
+# DELETE + INSERT
+do_patchconcat_test 4.3.7 {
+ DELETE FROM t2 WHERE b=1;
+} {
+ INSERT INTO t2 VALUES('x', 1, 2, '.');
+} {
+ {DELETE t2 0 .XX. {{} {} i 1 i 1 {} {}} {}}
+ {UPDATE t2 0 .XX. {{} {} i 1 i 2 {} {}} {t x {} {} {} {} t .}}
+}
+
+# DELETE + UPDATE
+do_patchconcat_test 4.3.8 -revert {
+ DELETE FROM t2 WHERE b=1 AND c=2;
+} {
+ UPDATE t2 SET a=5 WHERE b=1 AND c=2;
+} {
+ {DELETE t2 0 .XX. {{} {} i 1 i 2 {} {}} {}}
}
+
+# DELETE + UPDATE
+do_patchconcat_test 4.3.9 -revert {
+ DELETE FROM t2 WHERE b=1 AND c=2;
+} {
+ DELETE FROM t2 WHERE b=1;
+} {
+ {DELETE t2 0 .XX. {{} {} i 1 i 1 {} {}} {}}
+ {DELETE t2 0 .XX. {{} {} i 1 i 2 {} {}} {}}
}
finish_test
** hash key. Assume the has table has nBucket buckets. The hash keys
** calculated by this function are compatible with those calculated by
** sessionPreupdateHash().
+**
+** The bPkOnly argument is non-zero if the record at aRecord[] is from
+** a patchset DELETE. In this case the non-PK fields are omitted entirely.
*/
static unsigned int sessionChangeHash(
SessionTable *pTab, /* Table handle */
+ int bPkOnly, /* Record consists of PK fields only */
u8 *aRecord, /* Change record */
int nBucket /* Assume this many buckets in hash table */
){
for(i=0; i<pTab->nCol; i++){
int eType = *a;
int isPK = pTab->abPK[i];
+ if( bPkOnly && isPK==0 ) continue;
/* It is not possible for eType to be SQLITE_NULL here. The session
** module does not record changes for rows with NULL values stored in
*/
static int sessionChangeEqual(
SessionTable *pTab, /* Table used for PK definition */
+ int bLeftPkOnly,
u8 *aLeft, /* Change record */
+ int bRightPkOnly,
u8 *aRight /* Change record */
){
u8 *a1 = aLeft; /* Cursor to iterate through aLeft */
if( pTab->abPK[iCol] && (n1!=n2 || memcmp(a1, a2, n1)) ){
return 0;
}
- a1 += n1;
- a2 += n2;
+ if( pTab->abPK[iCol] || bLeftPkOnly==0 ) a1 += n1;
+ if( pTab->abPK[iCol] || bRightPkOnly==0 ) a2 += n2;
}
return 1;
static int sessionMergeUpdate(
u8 **paOut, /* IN/OUT: Pointer to output buffer */
SessionTable *pTab, /* Table change pertains to */
+ int bPatchset,
u8 *aOldRecord1, /* old.* record for first change */
u8 *aOldRecord2, /* old.* record for second change */
u8 *aNewRecord1, /* new.* record for first change */
u8 *aOut = *paOut;
int i;
- int bRequired = 0;
- assert( aOldRecord1 && aNewRecord1 );
-
- /* Write the old.* vector first. */
- for(i=0; i<pTab->nCol; i++){
- int nOld;
- u8 *aOld;
- int nNew;
- u8 *aNew;
-
- aOld = sessionMergeValue(&aOld1, &aOld2, &nOld);
- aNew = sessionMergeValue(&aNew1, &aNew2, &nNew);
- if( pTab->abPK[i] || nOld!=nNew || memcmp(aOld, aNew, nNew) ){
- if( pTab->abPK[i]==0 ) bRequired = 1;
- memcpy(aOut, aOld, nOld);
- aOut += nOld;
- }else{
- *(aOut++) = '\0';
+ if( bPatchset==0 ){
+ int bRequired = 0;
+
+ assert( aOldRecord1 && aNewRecord1 );
+
+ /* Write the old.* vector first. */
+ for(i=0; i<pTab->nCol; i++){
+ int nOld;
+ u8 *aOld;
+ int nNew;
+ u8 *aNew;
+
+ aOld = sessionMergeValue(&aOld1, &aOld2, &nOld);
+ aNew = sessionMergeValue(&aNew1, &aNew2, &nNew);
+ if( pTab->abPK[i] || nOld!=nNew || memcmp(aOld, aNew, nNew) ){
+ if( pTab->abPK[i]==0 ) bRequired = 1;
+ memcpy(aOut, aOld, nOld);
+ aOut += nOld;
+ }else{
+ *(aOut++) = '\0';
+ }
}
- }
- if( !bRequired ) return 0;
+ if( !bRequired ) return 0;
+ }
/* Write the new.* vector */
aOld1 = aOldRecord1;
aOld = sessionMergeValue(&aOld1, &aOld2, &nOld);
aNew = sessionMergeValue(&aNew1, &aNew2, &nNew);
- if( pTab->abPK[i] || (nOld==nNew && 0==memcmp(aOld, aNew, nNew)) ){
+ if( bPatchset==0
+ && (pTab->abPK[i] || (nOld==nNew && 0==memcmp(aOld, aNew, nNew)))
+ ){
*(aOut++) = '\0';
}else{
memcpy(aOut, aNew, nNew);
** Growing the hash table in this case is a performance optimization only,
** it is not required for correct operation.
*/
-static int sessionGrowHash(SessionTable *pTab){
+static int sessionGrowHash(int bPatchset, SessionTable *pTab){
if( pTab->nChange==0 || pTab->nEntry>=(pTab->nChange/2) ){
int i;
SessionChange **apNew;
SessionChange *p;
SessionChange *pNext;
for(p=pTab->apChange[i]; p; p=pNext){
- int iHash = sessionChangeHash(pTab, p->aRecord, nNew);
+ int bPkOnly = (p->op==SQLITE_DELETE && bPatchset);
+ int iHash = sessionChangeHash(pTab, bPkOnly, p->aRecord, nNew);
pNext = p->pNext;
p->pNext = apNew[iHash];
apNew[iHash] = p;
if( sessionInitTable(pSession, pTab) ) return;
/* Grow the hash table if required */
- if( sessionGrowHash(pTab) ){
+ if( sessionGrowHash(0, pTab) ){
pSession->rc = SQLITE_NOMEM;
return;
}
*/
static int sessionChangeMerge(
SessionTable *pTab, /* Table structure */
+ int bPatchset, /* True for patchsets */
SessionChange *pExist, /* Existing change */
int op2, /* Second change operation */
int bIndirect, /* True if second change is indirect */
sqlite3_free(pExist);
assert( pNew==0 );
}else{
+ u8 *aExist = pExist->aRecord;
int nByte;
u8 *aCsr;
+ /* Allocate a new SessionChange object. Ensure that the aRecord[]
+ ** buffer of the new object is large enough to hold any record that
+ ** may be generated by combining the input records. */
nByte = sizeof(SessionChange) + pExist->nRecord + nRec;
pNew = (SessionChange *)sqlite3_malloc(nByte);
if( !pNew ){
u8 *a1 = aRec;
assert( op2==SQLITE_UPDATE );
pNew->op = SQLITE_INSERT;
- sessionReadRecord(&a1, pTab->nCol, 0, 0);
- sessionMergeRecord(&aCsr, pTab->nCol, pExist->aRecord, a1);
+ if( bPatchset==0 ) sessionReadRecord(&a1, pTab->nCol, 0, 0);
+ sessionMergeRecord(&aCsr, pTab->nCol, aExist, a1);
}else if( op1==SQLITE_DELETE ){ /* DELETE + INSERT */
assert( op2==SQLITE_INSERT );
pNew->op = SQLITE_UPDATE;
- if( 0==sessionMergeUpdate(&aCsr, pTab, pExist->aRecord, 0, aRec, 0) ){
+ if( 0==sessionMergeUpdate(&aCsr, pTab, bPatchset, aExist, 0, aRec, 0) ){
sqlite3_free(pNew);
pNew = 0;
}
}else if( op2==SQLITE_UPDATE ){ /* UPDATE + UPDATE */
- u8 *a1 = pExist->aRecord;
+ u8 *a1 = aExist;
u8 *a2 = aRec;
assert( op1==SQLITE_UPDATE );
- sessionReadRecord(&a1, pTab->nCol, 0, 0);
- sessionReadRecord(&a2, pTab->nCol, 0, 0);
+ if( bPatchset==0 ){
+ sessionReadRecord(&a1, pTab->nCol, 0, 0);
+ sessionReadRecord(&a2, pTab->nCol, 0, 0);
+ }
pNew->op = SQLITE_UPDATE;
- if( 0==sessionMergeUpdate(&aCsr, pTab, aRec, pExist->aRecord, a1, a2) ){
+ if( 0==sessionMergeUpdate(&aCsr, pTab, bPatchset, aRec, aExist,a1,a2) ){
sqlite3_free(pNew);
pNew = 0;
}
}else{ /* UPDATE + DELETE */
assert( op1==SQLITE_UPDATE && op2==SQLITE_DELETE );
pNew->op = SQLITE_DELETE;
- sessionMergeRecord(&aCsr, pTab->nCol, aRec, pExist->aRecord);
+ if( bPatchset ){
+ memcpy(aCsr, aRec, nRec);
+ aCsr += nRec;
+ }else{
+ sessionMergeRecord(&aCsr, pTab->nCol, aRec, aExist);
+ }
}
if( pNew ){
** hash tables.
*/
static int sessionConcatChangeset(
+ int bPatchset, /* True to expect patchsets */
int nChangeset, /* Number of bytes in pChangeset */
void *pChangeset, /* Changeset buffer */
SessionTable **ppTabList /* IN/OUT: List of table objects */
SessionChange *pExist = 0;
SessionChange **pp;
+ assert( bPatchset==0 || bPatchset==1 );
+ assert( pIter->bPatchset==0 || pIter->bPatchset==1 );
+ if( pIter->bPatchset!=bPatchset ){
+ rc = SQLITE_ERROR;
+ break;
+ }
+
assert( pIter->apValue==0 );
sqlite3changeset_op(pIter, &zNew, &nCol, &op, &bIndirect);
pTab->zName = (char *)zNew;
}
- if( sessionGrowHash(pTab) ){
+ if( sessionGrowHash(bPatchset, pTab) ){
rc = SQLITE_NOMEM;
break;
}
- iHash = sessionChangeHash(pTab, aRec, pTab->nChange);
+ iHash = sessionChangeHash(
+ pTab, (bPatchset && op==SQLITE_DELETE), aRec, pTab->nChange
+ );
/* Search for existing entry. If found, remove it from the hash table.
** Code below may link it back in.
*/
for(pp=&pTab->apChange[iHash]; *pp; pp=&(*pp)->pNext){
- if( sessionChangeEqual(pTab, (*pp)->aRecord, aRec) ){
+ int bPkOnly1 = 0;
+ int bPkOnly2 = 0;
+ if( bPatchset ){
+ bPkOnly1 = (*pp)->op==SQLITE_DELETE;
+ bPkOnly2 = op==SQLITE_DELETE;
+ }
+ if( sessionChangeEqual(pTab, bPkOnly1, (*pp)->aRecord, bPkOnly2, aRec) ){
pExist = *pp;
*pp = (*pp)->pNext;
pTab->nEntry--;
}
}
- rc = sessionChangeMerge(pTab, pExist, op, bIndirect, aRec, nRec, &pChange);
+ rc = sessionChangeMerge(pTab,
+ bPatchset, pExist, op, bIndirect, aRec, nRec, &pChange
+ );
if( rc ) break;
if( pChange ){
pChange->pNext = pTab->apChange[iHash];
){
SessionTable *pList = 0; /* List of SessionTable objects */
int rc; /* Return code */
+ int bPatch; /* True for a patchset */
*pnOut = 0;
*ppOut = 0;
+ bPatch = (nLeft>0 && *(char*)pLeft=='P') || (nRight>0 && *(char*)pRight=='P');
- rc = sessionConcatChangeset(nLeft, pLeft, &pList);
+ rc = sessionConcatChangeset(bPatch, nLeft, pLeft, &pList);
if( rc==SQLITE_OK ){
- rc = sessionConcatChangeset(nRight, pRight, &pList);
+ rc = sessionConcatChangeset(bPatch, nRight, pRight, &pList);
}
/* Create the serialized output changeset based on the contents of the
int i;
if( pTab->nEntry==0 ) continue;
- sessionAppendTableHdr(&buf, 0, pTab, &rc);
+ sessionAppendTableHdr(&buf, bPatch, pTab, &rc);
for(i=0; i<pTab->nChange; i++){
SessionChange *p;
for(p=pTab->apChange[i]; p; p=p->pNext){