]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Start adding the sqlite3changeset_concat() function to the session module.
authordan <dan@noemail.net>
Thu, 14 Apr 2011 11:16:21 +0000 (11:16 +0000)
committerdan <dan@noemail.net>
Thu, 14 Apr 2011 11:16:21 +0000 (11:16 +0000)
FossilOrigin-Name: 8927b2260b8d84f53776cb29e1d2fa41b6b0de0e

ext/session/session5.test [new file with mode: 0644]
ext/session/sqlite3session.c
ext/session/sqlite3session.h
ext/session/test_session.c
manifest
manifest.uuid

diff --git a/ext/session/session5.test b/ext/session/session5.test
new file mode 100644 (file)
index 0000000..440a44c
--- /dev/null
@@ -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
index 7b77e170e8be407d63276edb4b48d30aa81875dd..770fc13e3047e92bb48d4ff88c0e40d97ba1eeb9 100644 (file)
@@ -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; i<pTab->nCol; 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; i<pTab->nCol; 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; i<pTab->nCol; 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; 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;
+
+  /* Write the new.* vector */
+  aOld1 = aOldRecord1;
+  aOld2 = aOldRecord2;
+  aNew1 = aNewRecord1;
+  aNew2 = aNewRecord2;
+  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 && 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; i<pTab->nChange; 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; i<pTab->nChange; 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; i<p->nCol*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 <concat> 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; i<pTab->nChange; 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 */
index eb4e114e37d3af63cd2d1f6afb906966729f5509..10a94b927b1697aafdbda8c02861505f00687168 100644 (file)
@@ -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
 **
index 6af1892150582824d616ae288cf5734a6f2c6273..05eb732d6deb9e7428fc3be5398d02d90e6a60b6 100644 (file)
@@ -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
   );
index 336c603f40421d4e7cd3d9eac10f98e5e5a41cca..4dcb9b0bb0d96b70f05c965d677420d966fc0861 100644 (file)
--- 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
index 1c2583a835f8834ce0b7594ce3119bb44b836f6d..4733d7436bfa26f18b83625f41e70319f4285c17 100644 (file)
@@ -1 +1 @@
-83705e90a54bad462a5b7fbca70cc129998f871c
\ No newline at end of file
+8927b2260b8d84f53776cb29e1d2fa41b6b0de0e
\ No newline at end of file