From 5d607a6e062a0b5f79b62091edd63967ea792edd Mon Sep 17 00:00:00 2001 From: dan Date: Thu, 14 Apr 2011 11:16:21 +0000 Subject: [PATCH] Start adding the sqlite3changeset_concat() function to the session module. FossilOrigin-Name: 8927b2260b8d84f53776cb29e1d2fa41b6b0de0e --- ext/session/session5.test | 124 ++++++++ ext/session/sqlite3session.c | 539 +++++++++++++++++++++++++++++++---- ext/session/sqlite3session.h | 6 + ext/session/test_session.c | 36 +++ manifest | 19 +- manifest.uuid | 2 +- 6 files changed, 660 insertions(+), 66 deletions(-) create mode 100644 ext/session/session5.test diff --git a/ext/session/session5.test b/ext/session/session5.test new file mode 100644 index 0000000000..440a44c08b --- /dev/null +++ b/ext/session/session5.test @@ -0,0 +1,124 @@ +# 2011 April 13 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# This file implements regression tests for the session module. +# Specifically, for the sqlite3changeset_concat() command. +# + +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 session5 + +proc do_concat_test {tn sql1 sql2} { + sqlite3session S1 db main ; S1 attach * + sqlite3session S2 db main ; S2 attach * + + execsql $sql1 + set C1 [S1 changeset] + S1 delete + + sqlite3session S1 db main ; S1 attach * + + execsql $sql2 + set C2 [S1 changeset] + S1 delete + + set C3 [S2 changeset] + S2 delete + + set C4 [sqlite3changeset_concat $C1 $C2] + + set c3 [list] + set c4 [list] + sqlite3session_foreach c $C3 { lappend c3 $c } + sqlite3session_foreach c $C4 { lappend c4 $c } + set c3 [lsort $c3] + set c4 [lsort $c4] + + do_test $tn [list set {} $c4] $c3 +} + +do_execsql_test 1.0 { + CREATE TABLE t1(a PRIMARY KEY, b); +} + +do_concat_test 1.1.1 { + INSERT INTO t1 VALUES(1, 'one'); +} { + INSERT INTO t1 VALUES(2, 'two'); +} + +do_concat_test 1.1.2 { + UPDATE t1 SET b = 'five' WHERE a = 1; +} { + UPDATE t1 SET b = 'six' WHERE a = 2; +} + +do_concat_test 1.1.3 { + DELETE FROM t1 WHERE a = 1; +} { + DELETE FROM t1 WHERE a = 2; +} + + +# 1.2.1: INSERT + DELETE -> (none) +# 1.2.2: INSERT + UPDATE -> INSERT +# +# 1.2.3: DELETE + INSERT (matching data) -> (none) +# 1.2.4: DELETE + INSERT (non-matching data) -> UPDATE +# +# 1.2.5: UPDATE + UPDATE (matching data) -> (none) +# 1.2.6: UPDATE + UPDATE (non-matching data) -> UPDATE +# 1.2.7: UPDATE + DELETE -> DELETE +# +do_concat_test 1.2.1 { + INSERT INTO t1 VALUES('x', 'y'); +} { + DELETE FROM t1 WHERE a = 'x'; +} +do_concat_test 1.2.2 { + INSERT INTO t1 VALUES(5.0, 'five'); +} { + UPDATE t1 SET b = 'six' WHERE a = 5.0; +} + +do_execsql_test 1.2.3.1 "INSERT INTO t1 VALUES('I', 'one')" +do_concat_test 1.2.3.2 { + DELETE FROM t1 WHERE a = 'I'; +} { + INSERT INTO t1 VALUES('I', 'one'); +} +do_concat_test 1.2.4 { + DELETE FROM t1 WHERE a = 'I'; +} { + INSERT INTO t1 VALUES('I', 'two'); +} +do_concat_test 1.2.5 { + UPDATE t1 SET b = 'five' WHERE a = 'I'; +} { + UPDATE t1 SET b = 'two' WHERE a = 'I'; +} +do_concat_test 1.2.6 { + UPDATE t1 SET b = 'six' WHERE a = 'I'; +} { + UPDATE t1 SET b = 'seven' WHERE a = 'I'; +} +do_concat_test 1.2.7 { + UPDATE t1 SET b = 'eight' WHERE a = 'I'; +} { + DELETE FROM t1 WHERE a = 'I'; +} + +finish_test diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c index 7b77e170e8..770fc13e30 100644 --- a/ext/session/sqlite3session.c +++ b/ext/session/sqlite3session.c @@ -126,6 +126,7 @@ struct SessionTable { ** Followed by one or more changes to the table. ** ** 1 byte: Either SQLITE_INSERT, UPDATE or DELETE. +** 1 byte: The "indirect-change" flag. ** old.* record: (delete and update only) ** new.* record: (insert and update only) */ @@ -353,20 +354,19 @@ static unsigned int sessionPreupdateHash( } /* -** Based on the primary key values stored in change pChange, calculate a +** Based on the primary key values stored in change aRecord, calculate a ** hash key, assuming the has table has nBucket buckets. The hash keys ** calculated by this function are compatible with those calculated by ** sessionPreupdateHash(). */ static unsigned int sessionChangeHash( - sqlite3 *db, /* Database handle */ SessionTable *pTab, /* Table handle */ - SessionChange *pChange, /* Change handle */ + u8 *aRecord, /* Change record */ int nBucket /* Assume this many buckets in hash table */ ){ unsigned int h = 0; /* Value to return */ int i; /* Used to iterate through columns */ - u8 *a = pChange->aRecord; /* Used to iterate through change record */ + u8 *a = aRecord; /* Used to iterate through change record */ for(i=0; inCol; i++){ int eType = *a++; @@ -393,6 +393,160 @@ static unsigned int sessionChangeHash( return (h % nBucket); } +static int sessionSerialLen(u8 *a){ + int e = *a; + int n; + if( e==0 ) return 1; + if( e==SQLITE_NULL ) return 1; + if( e==SQLITE_INTEGER || e==SQLITE_FLOAT ) return 9; + return sessionVarintGet(&a[1], &n) + 1 + n; +} + +static int sessionChangeEqual( + SessionTable *pTab, + u8 *aLeft, /* Change record */ + u8 *aRight /* Change record */ +){ + u8 *a1 = aLeft; + u8 *a2 = aRight; + int i; + + for(i=0; inCol; i++){ + int n1 = sessionSerialLen(a1); + int n2 = sessionSerialLen(a2); + + if( pTab->abPK[i] && (n1!=n2 || memcmp(a1, a2, n1)) ){ + return 0; + } + a1 += n1; + a2 += n1; + } + + return 1; +} + +static void sessionMergeRecord( + u8 **paOut, + SessionTable *pTab, + u8 *aLeft, + u8 *aRight +){ + u8 *a1 = aLeft; + u8 *a2 = aRight; + u8 *aOut = *paOut; + int i; + + for(i=0; inCol; i++){ + int n1 = sessionSerialLen(a1); + int n2 = sessionSerialLen(a2); + if( *a2 ){ + memcpy(aOut, a2, n2); + aOut += n2; + }else{ + memcpy(aOut, a1, n1); + aOut += n1; + } + a1 += n1; + a2 += n2; + } + + *paOut = aOut; +} + +static u8 *sessionMergeValue( + u8 **paOne, + u8 **paTwo, + int *pnVal +){ + u8 *a1 = *paOne; + u8 *a2 = *paTwo; + u8 *pRet = 0; + int n1; + + assert( a1 ); + if( a2 ){ + int n2 = sessionSerialLen(a2); + if( *a2 ){ + *pnVal = n2; + pRet = a2; + } + *paTwo = &a2[n2]; + } + + n1 = sessionSerialLen(a1); + if( pRet==0 ){ + *pnVal = n1; + pRet = a1; + } + *paOne = &a1[n1]; + + return pRet; +} + +static int sessionMergeUpdate( + u8 **paOut, + SessionTable *pTab, + u8 *aOldRecord1, + u8 *aOldRecord2, + u8 *aNewRecord1, + u8 *aNewRecord2 +){ + u8 *aOld1 = aOldRecord1; + u8 *aOld2 = aOldRecord2; + u8 *aNew1 = aNewRecord1; + u8 *aNew2 = aNewRecord2; + + u8 *aOut = *paOut; + int i; + int bRequired = 0; + + assert( aOldRecord1 && aNewRecord1 ); + + /* Write the old.* vector first. */ + for(i=0; inCol; 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; + + /* Write the new.* vector */ + aOld1 = aOldRecord1; + aOld2 = aOldRecord2; + aNew1 = aNewRecord1; + aNew2 = aNewRecord2; + for(i=0; inCol; 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 && 0==memcmp(aOld, aNew, nNew)) ){ + *(aOut++) = '\0'; + }else{ + memcpy(aOut, aNew, nNew); + aOut += nNew; + } + } + + *paOut = aOut; + return 1; +} + static int sessionPreupdateEqual( sqlite3 *db, SessionTable *pTab, @@ -480,7 +634,7 @@ static int sessionPreupdateEqual( ** Growing the hash table in this case is a performance optimization only, ** it is not required for correct operation. */ -static int sessionGrowHash(sqlite3_session *pSession, SessionTable *pTab){ +static int sessionGrowHash(SessionTable *pTab){ if( pTab->nChange==0 || pTab->nEntry>=(pTab->nChange/2) ){ int i; SessionChange **apNew; @@ -489,7 +643,6 @@ static int sessionGrowHash(sqlite3_session *pSession, SessionTable *pTab){ apNew = (SessionChange **)sqlite3_malloc(sizeof(SessionChange *) * nNew); if( apNew==0 ){ if( pTab->nChange==0 ){ - pSession->rc = SQLITE_NOMEM; return SQLITE_ERROR; } return SQLITE_OK; @@ -500,7 +653,7 @@ static int sessionGrowHash(sqlite3_session *pSession, SessionTable *pTab){ SessionChange *p; SessionChange *pNext; for(p=pTab->apChange[i]; p; p=pNext){ - int iHash = sessionChangeHash(pSession->db, pTab, p, nNew); + int iHash = sessionChangeHash(pTab, p->aRecord, nNew); pNext = p->pNext; p->pNext = apNew[iHash]; apNew[iHash] = p; @@ -677,7 +830,10 @@ static void sessionPreupdateOneChange( if( sessionInitTable(pSession, pTab) ) return; /* Grow the hash table if required */ - if( sessionGrowHash(pSession, pTab) ) return; + if( sessionGrowHash(pTab) ){ + pSession->rc = SQLITE_NOMEM; + return; + } /* Search the hash table for an existing entry for rowid=iKey2. If ** one is found, store a pointer to it in pChange and unlink it from @@ -693,8 +849,8 @@ static void sessionPreupdateOneChange( } if( pC==0 ){ /* Create a new change object containing all the old values (if - ** this is an SQLITE_UPDATE or SQLITE_DELETE), or just the PK - ** values (if this is an INSERT). */ + ** this is an SQLITE_UPDATE or SQLITE_DELETE), or just the PK + ** values (if this is an INSERT). */ SessionChange *pChange; /* New change object */ int nByte; /* Number of bytes to allocate */ int i; /* Used to iterate through columns */ @@ -851,6 +1007,27 @@ int sqlite3session_create( return SQLITE_OK; } +void sessionDeleteTable(SessionTable *pList){ + SessionTable *pNext; + SessionTable *pTab; + + for(pTab=pList; pTab; pTab=pNext){ + int i; + pNext = pTab->pNext; + for(i=0; inChange; i++){ + SessionChange *p; + SessionChange *pNext; + for(p=pTab->apChange[i]; p; p=pNext){ + pNext = p->pNext; + sqlite3_free(p); + } + } + sqlite3_free((char*)pTab->azCol); /* cast works around VC++ bug */ + sqlite3_free(pTab->apChange); + sqlite3_free(pTab); + } +} + /* ** Delete a session object previously allocated using sqlite3session_create(). */ @@ -870,22 +1047,7 @@ void sqlite3session_delete(sqlite3_session *pSession){ /* Delete all attached table objects. And the contents of their ** associated hash-tables. */ - while( pSession->pTable ){ - int i; - SessionTable *pTab = pSession->pTable; - pSession->pTable = pTab->pNext; - for(i=0; inChange; i++){ - SessionChange *p; - SessionChange *pNext; - for(p=pTab->apChange[i]; p; p=pNext){ - pNext = p->pNext; - sqlite3_free(p); - } - } - sqlite3_free((char*)pTab->azCol); /* cast works around VC++ bug */ - sqlite3_free(pTab->apChange); - sqlite3_free(pTab); - } + sessionDeleteTable(pSession->pTable); /* Free the session object itself. */ sqlite3_free(pSession); @@ -1322,6 +1484,18 @@ static int sessionSelectBind( return rc; } +static void sessionAppendTableHdr( + SessionBuffer *pBuf, + SessionTable *pTab, + int *pRc +){ + /* Write a table header */ + sessionAppendByte(pBuf, 'T', pRc); + sessionAppendVarint(pBuf, pTab->nCol, pRc); + sessionAppendBlob(pBuf, pTab->abPK, pTab->nCol, pRc); + sessionAppendBlob(pBuf, (u8 *)pTab->zName, strlen(pTab->zName)+1, pRc); +} + /* ** Obtain a changeset object containing all changes recorded by the ** session object passed as the first argument. @@ -1369,10 +1543,7 @@ int sqlite3session_changeset( } /* Write a table header */ - sessionAppendByte(&buf, 'T', &rc); - sessionAppendVarint(&buf, nCol, &rc); - sessionAppendBlob(&buf, pTab->abPK, nCol, &rc); - sessionAppendBlob(&buf, (u8 *)zName, sqlite3Strlen30(zName)+1, &rc); + sessionAppendTableHdr(&buf, pTab, &rc); /* Build and compile a statement to execute: */ if( rc==SQLITE_OK ){ @@ -1525,9 +1696,9 @@ static int sessionReadRecord( if( eType==SQLITE_TEXT || eType==SQLITE_BLOB ){ int nByte; - int enc = (eType==SQLITE_TEXT ? SQLITE_UTF8 : 0); aRec += sessionVarintGet(aRec, &nByte); if( apOut ){ + int enc = (eType==SQLITE_TEXT ? SQLITE_UTF8 : 0); sqlite3ValueSetStr(apOut[i], nByte, aRec, enc, SQLITE_STATIC); } aRec += nByte; @@ -1552,23 +1723,21 @@ static int sessionReadRecord( return SQLITE_OK; } -/* -** Advance an iterator created by sqlite3changeset_start() to the next -** change in the changeset. This function may return SQLITE_ROW, SQLITE_DONE -** or SQLITE_CORRUPT. -** -** This function may not be called on iterators passed to a conflict handler -** callback by changeset_apply(). -*/ -int sqlite3changeset_next(sqlite3_changeset_iter *p){ +static int sessionChangesetNext( + sqlite3_changeset_iter *p, + u8 **paRec, + int *pnRec +){ u8 *aChange; int i; u8 c; + assert( (paRec==0 && pnRec==0) || (paRec && pnRec) ); + /* If the iterator is in the error-state, return immediately. */ if( p->rc!=SQLITE_OK ) return p->rc; - /* Free the current contents of p->apValue[]. */ + /* Free the current contents of p->apValue[], if any. */ if( p->apValue ){ for(i=0; inCol*2; i++){ sqlite3ValueFree(p->apValue[i]); @@ -1582,47 +1751,64 @@ int sqlite3changeset_next(sqlite3_changeset_iter *p){ } aChange = p->pNext; - c = *(aChange++); - if( c=='T' ){ + if( aChange[0]=='T' ){ int nByte; /* Bytes to allocate for apValue */ + aChange++; aChange += sessionVarintGet(aChange, &p->nCol); p->abPK = (u8 *)aChange; aChange += p->nCol; p->zTab = (char *)aChange; aChange += (sqlite3Strlen30((char *)aChange) + 1); - p->op = *(aChange++); - p->bIndirect = *(aChange++); - sqlite3_free(p->apValue); - nByte = sizeof(sqlite3_value *) * p->nCol * 2; - p->apValue = (sqlite3_value **)sqlite3_malloc(nByte); - if( !p->apValue ){ - return (p->rc = SQLITE_NOMEM); + + if( paRec==0 ){ + sqlite3_free(p->apValue); + nByte = sizeof(sqlite3_value *) * p->nCol * 2; + p->apValue = (sqlite3_value **)sqlite3_malloc(nByte); + if( !p->apValue ){ + return (p->rc = SQLITE_NOMEM); + } + memset(p->apValue, 0, sizeof(sqlite3_value*)*p->nCol*2); } - memset(p->apValue, 0, sizeof(sqlite3_value*)*p->nCol*2); - }else{ - p->op = c; - p->bIndirect = *(aChange++); } + + p->op = *(aChange++); + p->bIndirect = *(aChange++); if( p->op!=SQLITE_UPDATE && p->op!=SQLITE_DELETE && p->op!=SQLITE_INSERT ){ return (p->rc = SQLITE_CORRUPT); } + if( paRec ){ *paRec = aChange; } + /* If this is an UPDATE or DELETE, read the old.* record. */ if( p->op!=SQLITE_INSERT ){ - p->rc = sessionReadRecord(&aChange, p->nCol, p->apValue); + p->rc = sessionReadRecord(&aChange, p->nCol, paRec?0:p->apValue); if( p->rc!=SQLITE_OK ) return p->rc; } /* If this is an INSERT or UPDATE, read the new.* record. */ if( p->op!=SQLITE_DELETE ){ - p->rc = sessionReadRecord(&aChange, p->nCol, &p->apValue[p->nCol]); + p->rc = sessionReadRecord(&aChange, p->nCol, paRec?0:&p->apValue[p->nCol]); if( p->rc!=SQLITE_OK ) return p->rc; } + if( pnRec ){ *pnRec = aChange - *paRec; } p->pNext = aChange; return SQLITE_ROW; } +/* +** Advance an iterator created by sqlite3changeset_start() to the next +** change in the changeset. This function may return SQLITE_ROW, SQLITE_DONE +** or SQLITE_CORRUPT. +** +** 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); + +} + /* ** The following function extracts information on the current change ** from a changeset iterator. They may only be called after changeset_next() @@ -2535,4 +2721,245 @@ int sqlite3changeset_apply( return rc; } +static int sessionChangeMerge( + SessionTable *pTab, + SessionChange *pExist, + int op2, + int bIndirect, + u8 *aRec, + int nRec, + SessionChange **ppNew +){ + SessionChange *pNew = 0; + + if( !pExist ){ + pNew = (SessionChange *)sqlite3_malloc(sizeof(SessionChange)); + if( !pNew ){ + return SQLITE_NOMEM; + } + memset(pNew, 0, sizeof(SessionChange)); + pNew->bInsert = op2; + pNew->bIndirect = bIndirect; + pNew->nRecord = nRec; + pNew->aRecord = aRec; + }else{ + int op1 = pExist->bInsert; + + /* + ** op1=INSERT, op2=INSERT -> Unsupported. Discard op2. + ** op1=INSERT, op2=UPDATE -> INSERT. + ** op1=INSERT, op2=DELETE -> (none) + ** + ** op1=UPDATE, op2=INSERT -> Unsupported. Discard op2. + ** op1=UPDATE, op2=UPDATE -> UPDATE. + ** op1=UPDATE, op2=DELETE -> DELETE. + ** + ** op1=DELETE, op2=INSERT -> UPDATE. + ** op1=DELETE, op2=UPDATE -> Unsupported. Discard op2. + ** op1=DELETE, op2=DELETE -> Unsupported. Discard op2. + */ + if( (op1==SQLITE_INSERT && op2==SQLITE_INSERT) + || (op1==SQLITE_UPDATE && op2==SQLITE_INSERT) + || (op1==SQLITE_DELETE && op2==SQLITE_UPDATE) + || (op1==SQLITE_DELETE && op2==SQLITE_DELETE) + ){ + pNew = pExist; + }else if( op1==SQLITE_INSERT && op2==SQLITE_DELETE ){ + sqlite3_free(pExist); + assert( pNew==0 ); + }else{ + int nByte; + u8 *aCsr; + + nByte = sizeof(SessionChange) + pExist->nRecord + nRec; + pNew = (SessionChange *)sqlite3_malloc(nByte); + if( !pNew ){ + return SQLITE_NOMEM; + } + memset(pNew, 0, sizeof(SessionChange)); + pNew->bIndirect = (bIndirect && pExist->bIndirect); + aCsr = pNew->aRecord = (u8 *)&pNew[1]; + + if( op1==SQLITE_INSERT && op2==SQLITE_UPDATE ){ + u8 *a1 = aRec; + pNew->bInsert = SQLITE_INSERT; + sessionReadRecord(&a1, pTab->nCol, 0); + sessionMergeRecord(&aCsr, pTab, pExist->aRecord, a1); + } + else if( op1==SQLITE_UPDATE && op2==SQLITE_UPDATE ){ + u8 *a1 = pExist->aRecord; + u8 *a2 = aRec; + sessionReadRecord(&a1, pTab->nCol, 0); + sessionReadRecord(&a2, pTab->nCol, 0); + pNew->bInsert = SQLITE_UPDATE; + if( 0==sessionMergeUpdate(&aCsr, pTab, aRec, pExist->aRecord, a1, a2) ){ + sqlite3_free(pNew); + pNew = 0; + } + } + else if( op1==SQLITE_UPDATE && op2==SQLITE_DELETE ){ + pNew->bInsert = SQLITE_DELETE; + sessionMergeRecord(&aCsr, pTab, aRec, pExist->aRecord); + } + else if( op1==SQLITE_DELETE && op2==SQLITE_INSERT ){ + pNew->bInsert = SQLITE_UPDATE; + if( 0==sessionMergeUpdate(&aCsr, pTab, pExist->aRecord, 0, aRec, 0) ){ + sqlite3_free(pNew); + pNew = 0; + } + } + + if( pNew ){ + pNew->nRecord = (aCsr - pNew->aRecord); + } + sqlite3_free(pExist); + } + } + + *ppNew = pNew; + return SQLITE_OK; +} + +int sessionConcatChangeset( + int nChangeset, + void *pChangeset, + SessionTable **ppTabList +){ + u8 *aRec; + int nRec; + sqlite3_changeset_iter *pIter; + int rc; + SessionTable *pTab = 0; + + rc = sqlite3changeset_start(&pIter, nChangeset, pChangeset); + if( rc!=SQLITE_OK ) return rc; + + while( SQLITE_ROW==sessionChangesetNext(pIter, &aRec, &nRec) ){ + const char *zNew; + int nCol; + int op; + int iHash; + int bIndirect; + SessionChange *pChange; + SessionChange *pExist = 0; + SessionChange **pp; + + assert( pIter->apValue==0 ); + sqlite3changeset_op(pIter, &zNew, &nCol, &op, &bIndirect); + + if( !pTab || zNew!=pTab->zName ){ + /* Search the list for a matching table */ + int nNew = strlen(zNew); + for(pTab = *ppTabList; pTab; pTab=pTab->pNext){ + if( 0==sqlite3_strnicmp(pTab->zName, zNew, nNew+1) ) break; + } + if( !pTab ){ + pTab = sqlite3_malloc(sizeof(SessionTable)); + if( !pTab ) break; + memset(pTab, 0, sizeof(SessionTable)); + pTab->pNext = *ppTabList; + *ppTabList = pTab; + } + pTab->zName = (char *)zNew; + pTab->nCol = nCol; + sqlite3changeset_pk(pIter, &pTab->abPK, 0); + } + + if( sessionGrowHash(pTab) ) break; + iHash = sessionChangeHash(pTab, 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) ){ + pExist = *pp; + *pp = (*pp)->pNext; + pTab->nEntry--; + break; + } + } + + rc = sessionChangeMerge(pTab, pExist, op, bIndirect, aRec, nRec, &pChange); + if( rc ) break; + if( pChange ){ + pChange->pNext = pTab->apChange[iHash]; + pTab->apChange[iHash] = pChange; + pTab->nEntry++; + } + } + + if( rc==SQLITE_OK ){ + rc = sqlite3changeset_finalize(pIter); + }else{ + sqlite3changeset_finalize(pIter); + } + return rc; +} + + +/* +** 1. Iterate through the left-hand changeset. Add an entry to a table +** specific hash table for each change in the changeset. The hash table +** key is the PK of the row affected by the change. +** +** 2. Then interate through the right-hand changeset. Attempt to add an +** entry to a hash table for each component change. If a change already +** exists with the same PK values, combine the two into a single change. +** +** 3. Write an output changeset based on the contents of the hash table. +*/ +int sqlite3changeset_concat( + int nLeft, /* Number of bytes in lhs input */ + void *pLeft, /* Lhs input changeset */ + int nRight /* Number of bytes in rhs input */, + void *pRight, /* Rhs input changeset */ + int *pnOut, /* OUT: Number of bytes in output changeset */ + void **ppOut /* OUT: changeset (left right) */ +){ + SessionTable *pList = 0; /* List of SessionTable objects */ + int rc; /* Return code */ + + *pnOut = 0; + *ppOut = 0; + + rc = sessionConcatChangeset(nLeft, pLeft, &pList); + if( rc==SQLITE_OK ){ + rc = sessionConcatChangeset(nRight, pRight, &pList); + } + + /* Create the serialized output changeset based on the contents of the + ** hash tables attached to the SessionTable objects in list pList. + */ + if( rc==SQLITE_OK ){ + SessionTable *pTab; + SessionBuffer buf = {0, 0, 0}; + for(pTab=pList; pTab; pTab=pTab->pNext){ + int i; + if( pTab->nEntry==0 ) continue; + + sessionAppendTableHdr(&buf, pTab, &rc); + for(i=0; inChange; i++){ + SessionChange *p; + for(p=pTab->apChange[i]; p; p=p->pNext){ + sessionAppendByte(&buf, p->bInsert, &rc); + sessionAppendByte(&buf, p->bIndirect, &rc); + sessionAppendBlob(&buf, p->aRecord, p->nRecord, &rc); + } + } + } + + if( rc==SQLITE_OK ){ + *ppOut = buf.aBuf; + *pnOut = buf.nBuf; + }else{ + sqlite3_free(buf.aBuf); + } + } + + concat_out: + sessionDeleteTable(pList); + return rc; +} + #endif /* SQLITE_ENABLE_SESSION && SQLITE_ENABLE_PREUPDATE_HOOK */ diff --git a/ext/session/sqlite3session.h b/ext/session/sqlite3session.h index eb4e114e37..10a94b927b 100644 --- a/ext/session/sqlite3session.h +++ b/ext/session/sqlite3session.h @@ -522,6 +522,12 @@ int sqlite3changeset_invert( int *pnOut, void **ppOut /* OUT: Inverse of input */ ); +int sqlite3changeset_concat( + int nLeft, void *pLeft, /* Input changeset */ + int nRight, void *Right, /* Input changeset */ + int *pnOut, void **ppOut /* OUT: Inverse of input */ +); + /* ** CAPI3REF: Apply A Changeset To A Database ** diff --git a/ext/session/test_session.c b/ext/session/test_session.c index 6af1892150..05eb732d6d 100644 --- a/ext/session/test_session.c +++ b/ext/session/test_session.c @@ -410,6 +410,39 @@ static int test_sqlite3changeset_invert( return TCL_OK; } +/* +** sqlite3changeset_concat LEFT RIGHT +*/ +static int test_sqlite3changeset_concat( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + int rc; /* Return code from changeset_invert() */ + void *aLeft; /* Input changeset */ + int nLeft; /* Size of buffer aChangeset in bytes */ + void *aRight; /* Input changeset */ + int nRight; /* Size of buffer aChangeset in bytes */ + void *aOut; /* Output changeset */ + int nOut; /* Size of buffer aOut in bytes */ + + if( objc!=3 ){ + Tcl_WrongNumArgs(interp, 1, objv, "LEFT RIGHT"); + return TCL_ERROR; + } + aLeft = (void *)Tcl_GetByteArrayFromObj(objv[1], &nLeft); + aRight = (void *)Tcl_GetByteArrayFromObj(objv[2], &nRight); + + rc = sqlite3changeset_concat(nLeft, aLeft, nRight, aRight, &nOut, &aOut); + if( rc!=SQLITE_OK ){ + return test_session_error(interp, rc); + } + Tcl_SetObjResult(interp, Tcl_NewByteArrayObj((unsigned char *)aOut, nOut)); + sqlite3_free(aOut); + return TCL_OK; +} + /* ** sqlite3session_foreach VARNAME CHANGESET SCRIPT */ @@ -514,6 +547,9 @@ int TestSession_Init(Tcl_Interp *interp){ Tcl_CreateObjCommand( interp, "sqlite3changeset_invert", test_sqlite3changeset_invert, 0, 0 ); + Tcl_CreateObjCommand( + interp, "sqlite3changeset_concat", test_sqlite3changeset_concat, 0, 0 + ); Tcl_CreateObjCommand( interp, "sqlite3changeset_apply", test_sqlite3changeset_apply, 0, 0 ); diff --git a/manifest b/manifest index 336c603f40..4dcb9b0bb0 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Merge\sthe\slatest\strunk\schanges\sinto\sthe\ssessions\sbranch. -D 2011-04-09T18:07:51.034 +C Start\sadding\sthe\ssqlite3changeset_concat()\sfunction\sto\sthe\ssession\smodule. +D 2011-04-14T11:16:21.630 F Makefile.arm-wince-mingw32ce-gcc d6df77f1f48d690bd73162294bbba7f59507c72f F Makefile.in 7a4d9524721d40ef9ee26f93f9bd6a51dba106f2 F Makefile.linux-gcc 91d710bdc4998cb015f39edf3cb314ec4f4d7e23 @@ -103,11 +103,12 @@ F ext/session/session1.test 7a92a2a6f531aef1e9764ffb7f983fb8b315376d F ext/session/session2.test c3e5f78d5eb988e35cc2ba9ce3678f706283cfdb F ext/session/session3.test bfa2376db7cbb2ac69496f84d93a8d81b13110d3 F ext/session/session4.test a6ed685da7a5293c5d6f99855bcf41dbc352ca84 +F ext/session/session5.test ed5025c96693d406fb13bcf330d1a962dcb68c24 F ext/session/session_common.tcl fb91560b6dbd086010df8b3a137a452f1ac21a28 F ext/session/sessionfault.test 2544a2e2ecad56e3c07a32c09799871d243c114c -F ext/session/sqlite3session.c bc6fc77d70d4d9994598b1daf0a43d48965b2155 -F ext/session/sqlite3session.h 9b91addc5bd1777137d4f1c0252da9fbe2d4618e -F ext/session/test_session.c 82e3fd7d94f485ea63bcfb15d636c95a01db97a9 +F ext/session/sqlite3session.c 124ac6d43ac5820add2e736b25432c3f6b5a733a +F ext/session/sqlite3session.h dc7c85fd27fa3a9a17b34e0951ed36cdced1bc67 +F ext/session/test_session.c f4d1dca94db71ec2177ee61eab51e718e58476d7 F install-sh 9d4de14ab9fb0facae2f48780b874848cbf2f895 x F ltmain.sh 3ff0879076df340d2e23ae905484d8c15d5fdea8 F main.mk f942406cb7df55d1aec40a88a7ae399b730cd94f @@ -937,7 +938,7 @@ F tool/speedtest8.c 2902c46588c40b55661e471d7a86e4dd71a18224 F tool/speedtest8inst1.c 293327bc76823f473684d589a8160bde1f52c14e F tool/split-sqlite3c.tcl d9be87f1c340285a3e081eb19b4a247981ed290c F tool/vdbe-compress.tcl d70ea6d8a19e3571d7ab8c9b75cba86d1173ff0f -P 29090b695a95feaba1b74f9894997083a060263a 1c2f0f8477bcf251fe874a2cfae4d7a403cb88ff -R a40e5f171b5b8b5d469c7b8e3eba3790 -U drh -Z 7e220d1108384435c7d9af48b2056273 +P 83705e90a54bad462a5b7fbca70cc129998f871c +R 2d3308935368f3ab85a5bea7204d6f97 +U dan +Z 4aed6546b387a7c5d8dbcb80467675b2 diff --git a/manifest.uuid b/manifest.uuid index 1c2583a835..4733d7436b 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -83705e90a54bad462a5b7fbca70cc129998f871c \ No newline at end of file +8927b2260b8d84f53776cb29e1d2fa41b6b0de0e \ No newline at end of file