]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Add start of sessions feature.
authordan <dan@noemail.net>
Tue, 8 Mar 2011 19:22:50 +0000 (19:22 +0000)
committerdan <dan@noemail.net>
Tue, 8 Mar 2011 19:22:50 +0000 (19:22 +0000)
FossilOrigin-Name: 269a81a37d7dbdcdec3c2322074916af0fbac91c

ext/session/sqlite3session.c [new file with mode: 0644]
ext/session/sqlite3session.h [new file with mode: 0644]
ext/session/test_session.c [new file with mode: 0644]
main.mk
manifest
manifest.uuid
src/tclsqlite.c
src/vdbeInt.h
src/vdbeapi.c
src/vdbeaux.c
test/session1.test [new file with mode: 0644]

diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c
new file mode 100644 (file)
index 0000000..eb02477
--- /dev/null
@@ -0,0 +1,993 @@
+
+#ifdef SQLITE_ENABLE_SESSION
+
+#include "sqlite3session.h"
+#include <assert.h>
+#include <string.h>
+
+#include "sqliteInt.h"
+#include "vdbeInt.h"
+
+typedef struct RowChange RowChange;
+typedef struct SessionTable SessionTable;
+typedef struct SessionChange SessionChange;
+
+#if 0
+#ifndef SQLITE_AMALGAMATION
+typedef unsigned char u8;
+typedef unsigned long u32;
+typedef sqlite3_uint64 u64;
+#endif
+#endif
+
+struct sqlite3_session {
+  sqlite3 *db;                    /* Database handle session is attached to */
+  char *zDb;                      /* Name of database session is attached to */
+  int rc;                         /* Non-zero if an error has occurred */
+  sqlite3_session *pNext;         /* Next session object on same db. */
+  SessionTable *pTable;           /* List of attached tables */
+};
+
+/*
+** Each session object maintains a set of the following structures, one
+** for each table the session object is monitoring. The structures are
+** stored in a linked list starting at sqlite3_session.pTable.
+**
+** The keys of the SessionTable.aChange[] hash table are all rows that have
+** been modified in any way since the session object was attached to the
+** table.
+**
+** The data associated with each hash-table entry is a structure containing
+** a subset of the initial values that the modified row contained at the
+** start of the session. Or no initial values if the row was inserted.
+*/
+struct SessionTable {
+  SessionTable *pNext;
+  char *zName;                    /* Local name of table */
+  int nCol;                       /* Number of columns in table zName */
+
+  /* Hash table of modified rows */
+  int nEntry;                     /* NUmber of entries in hash table */
+  int nChange;                    /* Size of apChange[] array */
+  SessionChange **apChange;       /* Hash table buckets */
+};
+
+/* 
+** RECORD FORMAT:
+**
+** The following record format is similar to (but not compatible with) that 
+** used in SQLite database files. This format is used as part of the 
+** change-set binary format, and so must be architecture independent.
+**
+** Unlike the SQLite database record format, each field is self-contained -
+** there is no separation of header and data. Each field begins with a
+** single byte describing its type, as follows:
+**
+**       0x00: Undefined value.
+**       0x01: Integer value.
+**       0x02: Real value.
+**       0x03: Text value.
+**       0x04: Blob value.
+**       0x05: SQL NULL value.
+**
+** Note that the above match the definitions of SQLITE_INTEGER, SQLITE_TEXT
+** and so on in sqlite3.h. For undefined and NULL values, the field consists
+** only of the single type byte. For other types of values, the type byte
+** is followed by:
+**
+**   Text values:
+**     A varint containing the number of bytes in the value (encoded using
+**     UTF-8). Followed by a buffer containing the UTF-8 representation
+**     of the text value. There is no nul terminator.
+**
+**   Blob values:
+**     A varint containing the number of bytes in the value, followed by
+**     a buffer containing the value itself.
+**
+**   Integer values:
+**     An 8-byte big-endian integer value.
+**
+**   Real values:
+**     An 8-byte big-endian IEEE 754-2008 real value.
+**
+** Varint values are encoded in the same way as varints in the SQLite 
+** record format.
+**
+** CHANGESET FORMAT:
+**
+** A changeset is a collection of DELETE, UPDATE and INSERT operations on
+** one or more tables. Operations on a single table are grouped together,
+** but may occur in any order (i.e. deletes, updates and inserts are all
+** mixed together).
+**
+** Each group of changes begins with a table header:
+**
+**   1 byte: Constant 0x54 (capital 'T')
+**   Varint: Big-endian integer set to the number of columns in the table.
+**   N bytes: Unqualified table name (encoded using UTF-8). Nul-terminated.
+**
+** Followed by one or more changes to the table.
+**
+**   1 byte: Either SQLITE_INSERT, UPDATE or DELETE.
+**   old.* record: (delete and update only)
+**   new.* record: (insert and update only)
+*/
+
+/*
+** For each row modified during a session, there exists a single instance of
+** this structure stored in a SessionTable.aChange[] hash table.
+*/
+struct SessionChange {
+  sqlite3_int64 iKey;             /* Key value */
+  int nRecord;                    /* Number of bytes in buffer aRecord[] */
+  u8 *aRecord;                    /* Buffer containing old.* record */
+  SessionChange *pNext;           /* For hash-table collisions */
+};
+
+
+static int sessionVarintPut(u8 *aBuf, u32 iVal){
+  if( (iVal & ~0x7F)==0 ){
+    if( aBuf ){
+      aBuf[0] = (u8)iVal;
+    }
+    return 1;
+  }
+  if( (iVal & ~0x3FFF)==0 ){
+    if( aBuf ){
+      aBuf[0] = ((iVal >> 7) & 0x7F) | 0x80;
+      aBuf[1] = iVal & 0x7F;
+    }
+    return 2;
+  }
+  if( aBuf ){
+    aBuf[0] = ((iVal >> 28) & 0x7F) | 0x80;
+    aBuf[1] = ((iVal >> 21) & 0x7F) | 0x80;
+    aBuf[2] = ((iVal >> 14) & 0x7F) | 0x80;
+    aBuf[3] = ((iVal >>  7) & 0x7F) | 0x80;
+    aBuf[4] = iVal & 0x7F;
+  }
+  return 5;
+}
+
+static int sessionVarintGet(u8 *aBuf, int *piVal){
+  int ret;
+  u64 v;
+  ret = (int)sqlite3GetVarint(aBuf, &v);
+  *piVal = (int)v;
+  return ret;
+}
+
+static sqlite3_int64 sessionGetI64(u8 *aRec){
+  return (((sqlite3_int64)aRec[0]) << 56)
+       + (((sqlite3_int64)aRec[1]) << 48)
+       + (((sqlite3_int64)aRec[2]) << 40)
+       + (((sqlite3_int64)aRec[3]) << 32)
+       + (((sqlite3_int64)aRec[4]) << 24)
+       + (((sqlite3_int64)aRec[5]) << 16)
+       + (((sqlite3_int64)aRec[6]) <<  8)
+       + (((sqlite3_int64)aRec[7]) <<  0);
+}
+
+/*
+** This function is used to serialize the contents of value pValue (see
+** comment titled "RECORD FORMAT" above).
+**
+** If it is non-NULL, the serialized form of the value is written to 
+** buffer aBuf. *pnWrite is set to the number of bytes written before
+** returning. Or, if aBuf is NULL, the only thing this function does is
+** set *pnWrite.
+**
+** If no error occurs, SQLITE_OK is returned. Or, if an OOM error occurs
+** within a call to sqlite3_value_text() (may fail if the db is utf-16)) 
+** SQLITE_NOMEM is returned.
+*/
+static int sessionSerializeValue(
+  u8 *aBuf,                       /* If non-NULL, write serialized value here */
+  sqlite3_value *pValue,          /* Value to serialize */
+  int *pnWrite                    /* IN/OUT: Increment by bytes written */
+){
+  int eType; 
+  int nByte;
+
+  eType = sqlite3_value_type(pValue);
+  if( aBuf ) aBuf[0] = eType;
+
+  switch( eType ){
+    case SQLITE_NULL: 
+      nByte = 1;
+      break;
+
+    case SQLITE_INTEGER: 
+    case SQLITE_FLOAT:
+      if( aBuf ){
+        /* TODO: SQLite does something special to deal with mixed-endian
+        ** floating point values (e.g. ARM7). This code probably should
+        ** too.  */
+        u64 i;
+        if( eType==SQLITE_INTEGER ){
+          i = (u64)sqlite3_value_int64(pValue);
+        }else{
+          double r;
+          assert( sizeof(double)==8 && sizeof(u64)==8 );
+          r = sqlite3_value_double(pValue);
+          memcpy(&i, &r, 8);
+        }
+        aBuf[1] = (i>>56) & 0xFF;
+        aBuf[2] = (i>>48) & 0xFF;
+        aBuf[3] = (i>>40) & 0xFF;
+        aBuf[4] = (i>>32) & 0xFF;
+        aBuf[5] = (i>>24) & 0xFF;
+        aBuf[6] = (i>>16) & 0xFF;
+        aBuf[7] = (i>> 8) & 0xFF;
+        aBuf[8] = (i>> 0) & 0xFF;
+      }
+      nByte = 9; 
+      break;
+
+    case SQLITE_TEXT: 
+    case SQLITE_BLOB: {
+      int n = sqlite3_value_bytes(pValue);
+      int nVarint = sessionVarintPut(0, n);
+      if( aBuf ){
+        sessionVarintPut(&aBuf[1], n);
+        memcpy(&aBuf[nVarint + 1], eType==SQLITE_TEXT ? 
+            sqlite3_value_text(pValue) : sqlite3_value_blob(pValue), n
+        );
+      }
+
+      nByte = 1 + nVarint + n;
+      break;
+    }
+  }
+
+  *pnWrite += nByte;
+  return SQLITE_OK;
+}
+
+/*
+** Return the hash of iKey, assuming there are nBucket hash buckets in
+** the hash table.
+*/
+static int sessionKeyhash(int nBucket, sqlite3_int64 iKey){
+  return (iKey % nBucket);
+}
+
+/*
+** If required, grow the hash table used to store changes on table pTab 
+** (part of the session pSession). If a fatal OOM error occurs, set the
+** session object to failed and return SQLITE_ERROR. Otherwise, return
+** SQLITE_OK.
+**
+** It is possible that a non-fatal OOM error occurs in this function. In
+** that case the hash-table does not grow, but SQLITE_OK is returned anyway.
+** 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){
+  if( pTab->nChange==0 || pTab->nEntry>=(pTab->nChange/2) ){
+    int i;
+    SessionChange **apNew;
+    int nNew = (pTab->nChange ? pTab->nChange : 128) * 2;
+
+    apNew = (SessionChange **)sqlite3_malloc(sizeof(SessionChange *) * nNew);
+    if( apNew==0 ){
+      if( pTab->nChange==0 ){
+        pSession->rc = SQLITE_NOMEM;
+        return SQLITE_ERROR;
+      }
+      return SQLITE_OK;
+    }
+    memset(apNew, 0, sizeof(SessionChange *) * nNew);
+
+    for(i=0; i<pTab->nChange; i++){
+      SessionChange *p;
+      SessionChange *pNext;
+      for(p=pTab->apChange[i]; p; p=pNext){
+        int iHash = sessionKeyhash(nNew, p->iKey);
+        pNext = p->pNext;
+        p->pNext = apNew[iHash];
+        apNew[iHash] = p;
+      }
+    }
+
+    sqlite3_free(pTab->apChange);
+    pTab->nChange = nNew;
+    pTab->apChange = apNew;
+  }
+
+  return SQLITE_OK;
+}
+
+static int sessionInitTable(sqlite3_session *pSession, SessionTable *pTab){
+  if( pTab->nCol==0 ){
+    pTab->nCol = sqlite3_preupdate_count(pSession->db);
+  }
+
+  if( pTab->nCol!=sqlite3_preupdate_count(pSession->db) ){
+    pSession->rc = SQLITE_SCHEMA;
+    return SQLITE_ERROR;
+  }
+
+  return SQLITE_OK;
+}
+
+/*
+** The 'pre-update' hook registered by this module with SQLite databases.
+*/
+static void xPreUpdate(
+  void *pCtx,                     /* Copy of third arg to preupdate_hook() */
+  sqlite3 *db,                    /* Database handle */
+  int op,                         /* SQLITE_UPDATE, DELETE or INSERT */
+  char const *zDb,                /* Database name */
+  char const *zName,              /* Table name */
+  sqlite3_int64 iKey1,            /* Rowid of row about to be deleted/updated */
+  sqlite3_int64 iKey2             /* New rowid value (for a rowid UPDATE) */
+){
+  sqlite3_session *pSession;
+  int nDb = strlen(zDb);
+  int nName = strlen(zDb);
+  for(pSession=(sqlite3_session *)pCtx; pSession; pSession=pSession->pNext){
+    SessionTable *pTab;
+    if( pSession->rc ) continue;
+    if( sqlite3_strnicmp(zDb, pSession->zDb, nDb+1) ) continue;
+    for(pTab=pSession->pTable; pTab; pTab=pTab->pNext){
+      if( 0==sqlite3_strnicmp(pTab->zName, zName, nName+1) ){
+        SessionChange *pChange;
+        SessionChange *pC;
+        int iHash; 
+        int rc = SQLITE_OK;
+
+        /* Load table details if required */
+        if( sessionInitTable(pSession, pTab) ) return;
+
+        /* Grow the hash table if required */
+        if( sessionGrowHash(pSession, pTab) ) 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
+        ** the hash table. Otherwise, set pChange to NULL.
+        */
+        iHash = sessionKeyhash(pTab->nChange, iKey2);
+        for(pC=pTab->apChange[iHash]; pC; pC=pC->pNext){
+          if( pC->iKey==iKey2 ) break;
+        }
+        if( pC ) continue;
+
+        pTab->nEntry++;
+
+        /* Create a new change object containing all the old values (if
+        ** this is an SQLITE_UPDATE or SQLITE_DELETE), or no record at
+        ** all (if this is an INSERT). */
+        if( op==SQLITE_INSERT ){
+          pChange = (SessionChange *)sqlite3_malloc(sizeof(SessionChange));
+          if( pChange ){
+            memset(pChange, 0, sizeof(SessionChange));
+          }
+        }else{
+          int nByte;            /* Number of bytes to allocate */
+          int i;                /* Used to iterate through columns */
+          sqlite3_value *pValue;
+
+          /* Figure out how large an allocation is required */
+          nByte = sizeof(SessionChange);
+          for(i=0; i<pTab->nCol && rc==SQLITE_OK; i++){
+            rc = sqlite3_preupdate_old(pSession->db, i, &pValue);
+            if( rc==SQLITE_OK ){
+              rc = sessionSerializeValue(0, pValue, &nByte);
+            }
+          }
+
+          /* Allocate the change object */
+          pChange = (SessionChange *)sqlite3_malloc(nByte);
+          if( !pChange ){
+            rc = SQLITE_NOMEM;
+          }else{
+            memset(pChange, 0, sizeof(SessionChange));
+            pChange->aRecord = (u8 *)&pChange[1];
+          }
+
+          /* Populate the change object */
+          nByte = 0;
+          for(i=0; i<pTab->nCol && rc==SQLITE_OK; i++){
+            rc = sqlite3_preupdate_old(pSession->db, i, &pValue);
+            if( rc==SQLITE_OK ){
+              rc = sessionSerializeValue(
+                  &pChange->aRecord[nByte], pValue, &nByte);
+            }
+          }
+          pChange->nRecord = nByte;
+        }
+
+        /* If an error has occurred, mark the session object as failed. */
+        if( rc!=SQLITE_OK ){
+          sqlite3_free(pChange);
+          pSession->rc = rc;
+          return;
+        }
+
+        /* Add the change back to the hash-table */
+        pChange->iKey = iKey2;
+        pChange->pNext = pTab->apChange[iHash];
+        pTab->apChange[iHash] = pChange;
+      }
+      break;
+    }
+  }
+}
+
+/*
+** Create a session object. This session object will record changes to
+** database zDb attached to connection db.
+*/
+int sqlite3session_create(
+  sqlite3 *db,                    /* Database handle */
+  const char *zDb,                /* Name of db (e.g. "main") */
+  sqlite3_session **ppSession     /* OUT: New session object */
+){
+  sqlite3_session *pNew;
+  sqlite3_session *pOld;
+  int nDb = strlen(zDb);          /* Length of zDb in bytes */
+
+  *ppSession = 0;
+
+  /* Allocate and populate the new session object. */
+  pNew = (sqlite3_session *)sqlite3_malloc(sizeof(sqlite3_session) + nDb + 1);
+  if( !pNew ) return SQLITE_NOMEM;
+  memset(pNew, 0, sizeof(sqlite3_session));
+  pNew->db = db;
+  pNew->zDb = (char *)&pNew[1];
+  memcpy(pNew->zDb, zDb, nDb+1);
+
+  /* Add the new session object to the linked list of session objects 
+  ** attached to database handle $db. Do this under the cover of the db
+  ** handle mutex.  */
+  sqlite3_mutex_enter(sqlite3_db_mutex(db));
+  pOld = (sqlite3_session*)sqlite3_preupdate_hook(db, xPreUpdate, (void*)pNew);
+  pNew->pNext = pOld;
+  sqlite3_mutex_leave(sqlite3_db_mutex(db));
+
+  *ppSession = pNew;
+  return SQLITE_OK;
+}
+
+/*
+** Delete a session object previously allocated using sqlite3session_create().
+*/
+void sqlite3session_delete(sqlite3_session *pSession){
+  sqlite3 *db = pSession->db;
+  sqlite3_session *pHead;
+  sqlite3_session **pp;
+
+  sqlite3_mutex_enter(sqlite3_db_mutex(db));
+  pHead = (sqlite3_session*)sqlite3_preupdate_hook(db, 0, 0);
+  for(pp=&pHead; (*pp)!=pSession; pp=&((*pp)->pNext));
+  *pp = (*pp)->pNext;
+  if( pHead ) sqlite3_preupdate_hook(db, xPreUpdate, (void *)pHead);
+  sqlite3_mutex_leave(sqlite3_db_mutex(db));
+
+  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(pTab->apChange);
+    sqlite3_free(pTab);
+  }
+
+  sqlite3_free(pSession);
+}
+
+/*
+** Attach a table to a session. All subsequent changes made to the table
+** while the session object is enabled will be recorded.
+**
+** Only tables that have a PRIMARY KEY defined may be attached. It does
+** not matter if the PRIMARY KEY is an "INTEGER PRIMARY KEY" (rowid alias)
+** or not.
+*/
+int sqlite3session_attach(
+  sqlite3_session *pSession,      /* Session object */
+  const char *zName               /* Table name */
+){
+  SessionTable *pTab;
+  int nName;
+
+  /* First search for an existing entry. If one is found, this call is
+  ** a no-op. Return early. */
+  nName = strlen(zName);
+  for(pTab=pSession->pTable; pTab; pTab=pTab->pNext){
+    if( 0==sqlite3_strnicmp(pTab->zName, zName, nName+1) ){
+      return SQLITE_OK;
+    }
+  }
+
+  /* Allocate new SessionTable object. */
+  pTab = (SessionTable *)sqlite3_malloc(sizeof(SessionTable) + nName + 1);
+  if( !pTab ) return SQLITE_NOMEM;
+
+  /* Populate the new SessionTable object and link it into the list. */
+  memset(pTab, 0, sizeof(SessionTable));
+  pTab->zName = (char *)&pTab[1];
+  memcpy(pTab->zName, zName, nName+1);
+  pTab->pNext = pSession->pTable;
+  pSession->pTable = pTab;
+
+  return SQLITE_OK;
+}
+
+typedef struct SessionBuffer SessionBuffer;
+struct SessionBuffer {
+  u8 *aBuf;                       /* Pointer to changeset buffer */
+  int nBuf;                       /* Size of buffer aBuf */
+  int nAlloc;                     /* Size of allocation containing aBuf */
+};
+
+static int sessionBufferGrow(SessionBuffer *p, int nByte, int *pRc){
+  if( p->nAlloc-p->nBuf<nByte ){
+    u8 *aNew;
+    int nNew = p->nAlloc ? p->nAlloc : 128;
+    do {
+      nNew = nNew*2;
+    }while( nNew<(p->nAlloc+nByte) );
+
+    aNew = (u8 *)sqlite3_realloc(p->aBuf, nNew);
+    if( 0==aNew ){
+      *pRc = SQLITE_NOMEM;
+      return 1;
+    }
+    p->aBuf = aNew;
+    p->nAlloc = nNew;
+  }
+  return 0;
+}
+
+static void sessionAppendByte(SessionBuffer *p, u8 v, int *pRc){
+  if( *pRc==SQLITE_OK && 0==sessionBufferGrow(p, 1, pRc) ){
+    p->aBuf[p->nBuf++] = v;
+  }
+}
+
+static void sessionAppendVarint(SessionBuffer *p, sqlite3_int64 v, int *pRc){
+  if( *pRc==SQLITE_OK && 0==sessionBufferGrow(p, 9, pRc) ){
+    p->nBuf += sessionVarintPut(&p->aBuf[p->nBuf], v);
+  }
+}
+
+static void sessionAppendBlob(
+  SessionBuffer *p, 
+  const u8 *aBlob, 
+  int nBlob, 
+  int *pRc
+){
+  if( *pRc==SQLITE_OK && 0==sessionBufferGrow(p, nBlob, pRc) ){
+    memcpy(&p->aBuf[p->nBuf], aBlob, nBlob);
+    p->nBuf += nBlob;
+  }
+}
+
+static void sessionAppendCol(
+  SessionBuffer *p, 
+  sqlite3_stmt *pStmt, 
+  int iCol,
+  int *pRc
+){
+  if( *pRc==SQLITE_OK ){
+    int eType = sqlite3_column_type(pStmt, iCol);
+    sessionAppendByte(p, (u8)eType, pRc);
+    if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){
+      sqlite3_int64 i;
+      u8 aBuf[8];
+      if( eType==SQLITE_INTEGER ){
+        i = sqlite3_column_int64(pStmt, iCol);
+      }else{
+        double r = sqlite3_column_double(pStmt, iCol);
+        memcpy(&i, &r, 8);
+      }
+      aBuf[0] = (i>>56) & 0xFF;
+      aBuf[1] = (i>>48) & 0xFF;
+      aBuf[2] = (i>>40) & 0xFF;
+      aBuf[3] = (i>>32) & 0xFF;
+      aBuf[4] = (i>>24) & 0xFF;
+      aBuf[5] = (i>>16) & 0xFF;
+      aBuf[6] = (i>> 8) & 0xFF;
+      aBuf[7] = (i>> 0) & 0xFF;
+      sessionAppendBlob(p, aBuf, 8, pRc);
+    }
+    if( eType==SQLITE_BLOB || eType==SQLITE_TEXT ){
+      int nByte = sqlite3_column_bytes(pStmt, iCol);
+      sessionAppendVarint(p, nByte, pRc);
+      sessionAppendBlob(p, eType==SQLITE_BLOB ? 
+        sqlite3_column_blob(pStmt, iCol) : sqlite3_column_text(pStmt, iCol),
+        nByte, pRc
+      );
+    }
+  }
+}
+
+static void sessionAppendUpdate(
+  sqlite3_stmt *pStmt, 
+  SessionBuffer *pBuf,
+  SessionChange *p,
+  int *pRc
+){
+  if( *pRc==SQLITE_OK ){
+    SessionBuffer buf2 = {0, 0, 0};
+    int bNoop = 1;
+    int i;
+    u8 *pCsr = p->aRecord;
+    sessionAppendByte(pBuf, SQLITE_UPDATE, pRc);
+    for(i=0; i<sqlite3_column_count(pStmt); i++){
+      int nCopy = 0;
+      int nAdvance;
+      int eType = *pCsr;
+      switch( eType ){
+        case SQLITE_NULL:
+          nAdvance = 1;
+          if( sqlite3_column_type(pStmt, i)!=SQLITE_NULL ){
+            nCopy = 1;
+          }
+          break;
+
+        case SQLITE_FLOAT:
+        case SQLITE_INTEGER: {
+          nAdvance = 9;
+          if( eType==sqlite3_column_type(pStmt, i) ){
+            sqlite3_int64 iVal = sessionGetI64(&pCsr[1]);
+            if( eType==SQLITE_INTEGER ){
+              if( iVal==sqlite3_column_int64(pStmt, i) ) break;
+            }else{
+              double dVal;
+              memcpy(&dVal, &iVal, 8);
+              if( dVal==sqlite3_column_double(pStmt, i) ) break;
+            }
+          }
+          nCopy = 9;
+          break;
+        }
+
+        case SQLITE_TEXT:
+        case SQLITE_BLOB: {
+          int nByte;
+          int nHdr = 1 + sessionVarintGet(&pCsr[1], &nByte);
+          nAdvance = nHdr + nByte;
+          if( eType==sqlite3_column_type(pStmt, i) 
+           && nByte==sqlite3_column_bytes(pStmt, i) 
+           && 0==memcmp(&pCsr[nHdr], sqlite3_column_blob(pStmt, i), nByte)
+          ){
+            break;
+          }
+          nCopy = nAdvance;
+        }
+      }
+
+      if( nCopy==0 ){
+        sessionAppendByte(pBuf, 0, pRc);
+        sessionAppendByte(&buf2, 0, pRc);
+      }else{
+        sessionAppendBlob(pBuf, pCsr, nCopy, pRc);
+        sessionAppendCol(&buf2, pStmt, i, pRc);
+        bNoop = 0;
+      }
+      pCsr += nAdvance;
+    }
+
+    if( bNoop ){
+      pBuf->nBuf -= (1 + sqlite3_column_count(pStmt));
+    }else{
+      sessionAppendBlob(pBuf, buf2.aBuf, buf2.nBuf, pRc);
+      sqlite3_free(buf2.aBuf);
+    }
+  }
+
+
+}
+
+/*
+** Obtain a changeset object containing all changes recorded by the 
+** session object passed as the first argument.
+**
+** It is the responsibility of the caller to eventually free the buffer 
+** using sqlite3_free().
+*/
+int sqlite3session_changeset(
+  sqlite3_session *pSession,      /* Session object */
+  int *pnChangeset,               /* OUT: Size of buffer at *ppChangeset */
+  void **ppChangeset              /* OUT: Buffer containing changeset */
+){
+  sqlite3 *db = pSession->db;
+  SessionTable *pTab;
+  SessionBuffer buf = {0, 0, 0};
+  int rc;
+
+  *pnChangeset = 0;
+  *ppChangeset = 0;
+  rc = pSession->rc;
+
+  for(pTab=pSession->pTable; rc==SQLITE_OK && pTab; pTab=pTab->pNext){
+    if( pTab->nEntry ){
+      int i;
+      sqlite3_stmt *pStmt = 0;
+      int bNoop = 1;
+      int nRewind = buf.nBuf;
+
+      /* Write a table header */
+      sessionAppendByte(&buf, 'T', &rc);
+      sessionAppendVarint(&buf, pTab->nCol, &rc);
+      sessionAppendBlob(&buf, (u8 *)pTab->zName, strlen(pTab->zName)+1, &rc);
+
+      /* Build and compile a statement to execute: */
+      if( rc==SQLITE_OK ){
+        char *zSql = sqlite3_mprintf("SELECT * FROM %Q.%Q WHERE _rowid_ = ?", 
+            pSession->zDb, pTab->zName
+        );
+        if( !zSql ){
+          rc = SQLITE_NOMEM;
+        }else{
+          rc = sqlite3_prepare_v2(db, zSql, -1, &pStmt, 0);
+        }
+        sqlite3_free(zSql);
+      }
+
+      if( rc==SQLITE_OK && pTab->nCol!=sqlite3_column_count(pStmt) ){
+        rc = SQLITE_SCHEMA;
+      }
+
+      for(i=0; i<pTab->nChange; i++){
+        SessionChange *p;
+        for(p=pTab->apChange[i]; rc==SQLITE_OK && p; p=p->pNext){
+          sqlite3_bind_int64(pStmt, 1, p->iKey);
+          if( sqlite3_step(pStmt)==SQLITE_ROW ){
+            int iCol;
+            if( p->aRecord ){
+              sessionAppendUpdate(pStmt, &buf, p, &rc);
+            }else{
+              sessionAppendByte(&buf, SQLITE_INSERT, &rc);
+              for(iCol=0; iCol<pTab->nCol; iCol++){
+                sessionAppendCol(&buf, pStmt, iCol, &rc);
+              }
+            }
+            bNoop = 0;
+          }else if( p->aRecord ){
+            /* A DELETE change */
+            sessionAppendByte(&buf, SQLITE_DELETE, &rc);
+            sessionAppendBlob(&buf, p->aRecord, p->nRecord, &rc);
+            bNoop = 0;
+          }
+          rc = sqlite3_reset(pStmt);
+        }
+      }
+
+      sqlite3_finalize(pStmt);
+
+      if( bNoop ){
+        buf.nBuf = nRewind;
+      }
+    }
+  }
+
+  if( rc==SQLITE_OK ){
+    *pnChangeset = buf.nBuf;
+    *ppChangeset = buf.aBuf;
+  }else{
+    sqlite3_free(buf.aBuf);
+  }
+
+  return rc;
+}
+
+int sqlite3session_enable(sqlite3_session *pSession, int bEnable){
+  return bEnable;
+}
+
+/************************************************************************/
+/************************************************************************/
+/************************************************************************/
+
+struct sqlite3_changeset_iter {
+  u8 *aChangeset;                 /* Pointer to buffer containing changeset */
+  int nChangeset;                 /* Number of bytes in aChangeset */
+  u8 *pNext;                      /* Pointer to next change within aChangeset */
+  int rc;
+
+  char *zTab;                     /* Current table */
+  int nCol;                       /* Number of columns in zTab */
+  int op;                         /* Current operation */
+  sqlite3_value **apValue;        /* old.* and new.* values */
+};
+
+/*
+** Create an iterator used to iterate through the contents of a changeset.
+*/
+int sqlite3changeset_start(
+  sqlite3_changeset_iter **ppIter,
+  int nChangeset, 
+  void *pChangeset
+){
+  sqlite3_changeset_iter *pRet;   /* Iterator to return */
+  int nByte;                      /* Number of bytes to allocate for iterator */
+
+  *ppIter = 0;
+
+  nByte = sizeof(sqlite3_changeset_iter);
+  pRet = (sqlite3_changeset_iter *)sqlite3_malloc(nByte);
+  if( !pRet ) return SQLITE_NOMEM;
+  memset(pRet, 0, sizeof(sqlite3_changeset_iter));
+
+  pRet->aChangeset = (u8 *)pChangeset;
+  pRet->nChangeset = nChangeset;
+  pRet->pNext = pRet->aChangeset;
+
+  *ppIter = pRet;
+  return SQLITE_OK;
+}
+
+static int sessionReadRecord(
+  u8 **paChange,                  /* IN/OUT: Pointer to binary record */
+  int nCol,                       /* Number of values in record */
+  sqlite3_value **apOut           /* Write values to this array */
+){
+  int i;
+  u8 *aRec = *paChange;
+
+  for(i=0; i<nCol; i++){
+    int eType = *aRec++;
+    assert( apOut[i]==0 );
+    if( eType ){
+      apOut[i] = sqlite3ValueNew(0);
+      if( !apOut[i] ) return SQLITE_NOMEM;
+
+      if( eType==SQLITE_TEXT || eType==SQLITE_BLOB ){
+        int nByte;
+        int enc = (eType==SQLITE_TEXT ? SQLITE_UTF8 : 0);
+        aRec += sessionVarintGet(aRec, &nByte);
+        sqlite3ValueSetStr(apOut[i], nByte, aRec, enc, SQLITE_STATIC);
+        aRec += nByte;
+      }
+      if( eType==SQLITE_INTEGER || eType==SQLITE_FLOAT ){
+        sqlite3_int64 v = sessionGetI64(aRec);
+        aRec += 8;
+        if( eType==SQLITE_INTEGER ){
+          sqlite3VdbeMemSetInt64(apOut[i], v);
+        }else{
+          double d;
+          memcpy(&d, &i, 8);
+          sqlite3VdbeMemSetDouble(apOut[i], d);
+        }
+      }
+    }
+  }
+
+  *paChange = aRec;
+  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){
+  u8 *aChange;
+  int i;
+  u8 c;
+
+  if( p->rc!=SQLITE_OK ) return p->rc;
+
+  if( p->apValue ){
+    for(i=0; i<p->nCol*2; i++){
+      sqlite3ValueFree(p->apValue[i]);
+    }
+    memset(p->apValue, 0, sizeof(sqlite3_value*)*p->nCol*2);
+  }
+
+  /* If the iterator is already at the end of the changeset, return DONE. */
+  if( p->pNext>=&p->aChangeset[p->nChangeset] ){
+    return SQLITE_DONE;
+  }
+  aChange = p->pNext;
+
+  c = *(aChange++);
+  if( c=='T' ){
+    int nByte;                    /* Bytes to allocate for apValue */
+    aChange += sessionVarintGet(aChange, &p->nCol);
+    p->zTab = (char *)aChange;
+    aChange += (strlen((char *)aChange) + 1);
+    p->op = *(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);
+    }
+    memset(p->apValue, 0, sizeof(sqlite3_value*)*p->nCol*2);
+  }else{
+    p->op = c;
+  }
+  if( p->op!=SQLITE_UPDATE && p->op!=SQLITE_DELETE && p->op!=SQLITE_INSERT ){
+    return (p->rc = SQLITE_CORRUPT);
+  }
+
+  /* If this is an UPDATE or DELETE, read the old.* record. */
+  if( p->op!=SQLITE_INSERT ){
+    p->rc = sessionReadRecord(&aChange, p->nCol, 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]);
+    if( p->rc!=SQLITE_OK ) return p->rc;
+  }
+
+  p->pNext = aChange;
+  return SQLITE_ROW;
+}
+
+/*
+** The following three functions extract information on the current change
+** from a changeset iterator. They may only be called after changeset_next()
+** has returned SQLITE_ROW.
+*/
+int sqlite3changeset_op(
+  sqlite3_changeset_iter *pIter,
+  const char **pzTab,                 /* OUT: Pointer to table name */
+  int *pnCol,                         /* OUT: Number of columns in table */
+  int *pOp                            /* OUT: SQLITE_INSERT, DELETE or UPDATE */
+){
+  *pOp = pIter->op;
+  *pnCol = pIter->nCol;
+  *pzTab = pIter->zTab;
+  return SQLITE_OK;
+}
+
+int sqlite3changeset_old(
+  sqlite3_changeset_iter *pIter,
+  int iVal,
+  sqlite3_value **ppValue             /* OUT: Old value (or NULL pointer) */
+){
+  if( iVal<0 || iVal>=pIter->nCol ){
+    return SQLITE_RANGE;
+  }
+  *ppValue = pIter->apValue[iVal];
+  return SQLITE_OK;
+}
+
+int sqlite3changeset_new(
+  sqlite3_changeset_iter *pIter,
+  int iVal,
+  sqlite3_value **ppValue             /* OUT: New value (or NULL pointer) */
+){
+  if( iVal<0 || iVal>=pIter->nCol ){
+    return SQLITE_RANGE;
+  }
+  *ppValue = pIter->apValue[pIter->nCol+iVal];
+  return SQLITE_OK;
+}
+
+/*
+** Finalize an iterator allocated with sqlite3changeset_start().
+**
+** This function may not be called on iterators passed to a conflict handler
+** callback by changeset_apply().
+*/
+int sqlite3changeset_finalize(sqlite3_changeset_iter *p){
+  int i;
+  int rc = p->rc;
+  for(i=0; i<p->nCol*2; i++) sqlite3ValueFree(p->apValue[i]);
+  sqlite3_free(p->apValue);
+  sqlite3_free(p);
+  return rc;
+}
+
+#endif        /* #ifdef SQLITE_ENABLE_SESSION */
diff --git a/ext/session/sqlite3session.h b/ext/session/sqlite3session.h
new file mode 100644 (file)
index 0000000..a62b8b6
--- /dev/null
@@ -0,0 +1,125 @@
+
+#ifndef __SQLITESESSION_H_
+#define __SQLITESESSION_H_ 1
+
+/*
+** Make sure we can call this stuff from C++.
+*/
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "sqlite3.h"
+
+typedef struct sqlite3_session sqlite3_session;
+typedef struct sqlite3_changeset_iter sqlite3_changeset_iter;
+
+/*
+** Create a session object. This session object will record changes to
+** database zDb attached to connection db.
+*/
+int sqlite3session_create(
+  sqlite3 *db,                    /* Database handle */
+  const char *zDb,                /* Name of db (e.g. "main") */
+  sqlite3_session **ppSession     /* OUT: New session object */
+);
+
+/*
+** Enable or disable the recording of changes by a session object. When
+** enabled, a session object records changes made to the database. When
+** disabled - it does not. A newly created session object is enabled.
+**
+** Passing zero to this function disables the session. Passing a value
+** greater than zero enables it. Passing a value less than zero is a 
+** no-op, and may be used to query the current state of the session.
+**
+** The return value indicates the final state of the session object: 0 if 
+** the session is disabled, or 1 if it is enabled.
+*/
+int sqlite3session_enable(sqlite3_session *pSession, int bEnable);
+
+/*
+** Attach a table to a session. All subsequent changes made to the table
+** while the session object is enabled will be recorded.
+**
+** Only tables that have a PRIMARY KEY defined may be attached. It does
+** not matter if the PRIMARY KEY is an "INTEGER PRIMARY KEY" (rowid alias)
+** or not.
+*/
+int sqlite3session_attach(
+  sqlite3_session *pSession,      /* Session object */
+  const char *zTab                /* Table name */
+);
+
+/*
+** Obtain a changeset object containing all changes recorded by the 
+** session object passed as the first argument.
+**
+** It is the responsibility of the caller to eventually free the buffer 
+** using sqlite3_free().
+*/
+int sqlite3session_changeset(
+  sqlite3_session *pSession,      /* Session object */
+  int *pnChangeset,               /* OUT: Size of buffer at *ppChangeset */
+  void **ppChangeset              /* OUT: Buffer containing changeset */
+);
+
+/*
+** Delete a session object previously allocated using sqlite3session_create().
+*/
+void sqlite3session_delete(sqlite3_session *pSession);
+
+
+/*
+** Create an iterator used to iterate through the contents of a changeset.
+*/
+int sqlite3changeset_start(
+  sqlite3_changeset_iter **ppIter,
+  int nChangeset, 
+  void *pChangeset
+);
+
+/*
+** 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 *pIter);
+
+/*
+** The following three functions extract information on the current change
+** from a changeset iterator. They may only be called after changeset_next()
+** has returned SQLITE_ROW.
+*/
+int sqlite3changeset_op(
+  sqlite3_changeset_iter *pIter,      /* Iterator object */
+  const char **pzTab,                 /* OUT: Pointer to table name */
+  int *pnCol,                         /* OUT: Number of columns in table */
+  int *pOp                            /* OUT: SQLITE_INSERT, DELETE or UPDATE */
+);
+
+int sqlite3changeset_old(
+  sqlite3_changeset_iter *pIter,
+  int iVal,
+  sqlite3_value **ppValue             /* OUT: Old value (or NULL pointer) */
+);
+
+int sqlite3changeset_new(
+  sqlite3_changeset_iter *pIter,
+  int iVal,
+  sqlite3_value **ppValue             /* OUT: New value (or NULL pointer) */
+);
+
+/*
+** Finalize an iterator allocated with sqlite3changeset_start().
+**
+** This function may not be called on iterators passed to a conflict handler
+** callback by changeset_apply().
+*/
+int sqlite3changeset_finalize(sqlite3_changeset_iter *pIter);
+
+#endif
+
diff --git a/ext/session/test_session.c b/ext/session/test_session.c
new file mode 100644 (file)
index 0000000..9c37fdb
--- /dev/null
@@ -0,0 +1,258 @@
+
+#if defined(SQLITE_TEST) && defined(SQLITE_ENABLE_SESSION)
+
+#include "sqlite3session.h"
+#include <assert.h>
+#include <string.h>
+#include <tcl.h>
+
+static int test_session_error(Tcl_Interp *interp, int rc){
+  extern const char *sqlite3TestErrorName(int);
+  Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3TestErrorName(rc), -1));
+  return TCL_ERROR;
+}
+
+/*
+** Tclcmd:  $session attach TABLE
+**          $session changeset
+**          $session delete
+**          $session enable BOOL
+*/
+static int test_session_cmd(
+  void *clientData,
+  Tcl_Interp *interp,
+  int objc,
+  Tcl_Obj *CONST objv[]
+){
+  sqlite3_session *pSession = (sqlite3_session *)clientData;
+  struct SessionSubcmd {
+    const char *zSub;
+    int nArg;
+    const char *zMsg;
+    int iSub;
+  } aSub[] = {
+    { "attach",    1, "TABLE", }, /* 0 */
+    { "changeset", 0, "",      }, /* 1 */
+    { "delete",    0, "",      }, /* 2 */
+    { "enable",    1, "",      }, /* 3 */
+    { 0 }
+  };
+  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;
+  }
+
+  switch( iSub ){
+    case 0:        /* attach */
+      rc = sqlite3session_attach(pSession, Tcl_GetString(objv[2]));
+      if( rc!=SQLITE_OK ){
+        return test_session_error(interp, rc);
+      }
+      break;
+
+    case 1: {      /* changeset */
+      int nChange;
+      void *pChange;
+      rc = sqlite3session_changeset(pSession, &nChange, &pChange);
+      if( rc==SQLITE_OK ){
+        Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(pChange, nChange)); 
+        sqlite3_free(pChange);
+      }else{
+        return test_session_error(interp, rc);
+      }
+      break;
+    }
+
+    case 2:        /* delete */
+      Tcl_DeleteCommand(interp, Tcl_GetString(objv[0]));
+      break;
+
+    case 3: {      /* enable */
+      int val;
+      if( Tcl_GetBooleanFromObj(interp, objv[2], &val) ) return TCL_ERROR;
+      val = sqlite3session_enable(pSession, val);
+      Tcl_SetObjResult(interp, Tcl_NewBooleanObj(val));
+      break;
+    }
+  }
+
+  return TCL_OK;
+}
+
+static void test_session_del(void *clientData){
+  sqlite3_session *pSession = (sqlite3_session *)clientData;
+  sqlite3session_delete(pSession);
+}
+
+/*
+** Tclcmd:  sqlite3session CMD DB-HANDLE DB-NAME
+*/
+static int test_sqlite3session(
+  void * clientData,
+  Tcl_Interp *interp,
+  int objc,
+  Tcl_Obj *CONST objv[]
+){
+  sqlite3 *db;
+  Tcl_CmdInfo info;
+  int rc;                         /* sqlite3session_create() return code */
+  sqlite3_session *pSession;      /* New session object */
+
+  if( objc!=4 ){
+    Tcl_WrongNumArgs(interp, 1, objv, "CMD DB-HANDLE DB-NAME");
+    return TCL_ERROR;
+  }
+
+  if( 0==Tcl_GetCommandInfo(interp, Tcl_GetString(objv[2]), &info) ){
+    Tcl_AppendResult(interp, "no such handle: ", Tcl_GetString(objv[2]), 0);
+    return TCL_ERROR;
+  }
+  db = *(sqlite3 **)info.objClientData;
+
+  rc = sqlite3session_create(db, Tcl_GetString(objv[3]), &pSession);
+  if( rc!=SQLITE_OK ){
+    return test_session_error(interp, rc);
+  }
+
+  Tcl_CreateObjCommand(
+      interp, Tcl_GetString(objv[1]), test_session_cmd, (ClientData)pSession,
+      test_session_del
+  );
+  Tcl_SetObjResult(interp, objv[1]);
+  return TCL_OK;
+}
+
+static void test_append_value(Tcl_Obj *pList, sqlite3_value *pVal){
+  if( pVal==0 ){
+    Tcl_ListObjAppendElement(0, pList, Tcl_NewObj());
+    Tcl_ListObjAppendElement(0, pList, Tcl_NewObj());
+  }else{
+    Tcl_Obj *pObj;
+    switch( sqlite3_value_type(pVal) ){
+      case SQLITE_NULL:
+        Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("n", 1));
+        pObj = Tcl_NewObj();
+        break;
+      case SQLITE_INTEGER:
+        Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("i", 1));
+        pObj = Tcl_NewWideIntObj(sqlite3_value_int64(pVal));
+        break;
+      case SQLITE_FLOAT:
+        Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("f", 1));
+        pObj = Tcl_NewDoubleObj(sqlite3_value_double(pVal));
+        break;
+      case SQLITE_TEXT:
+        Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("t", 1));
+        pObj = Tcl_NewStringObj((char *)sqlite3_value_text(pVal), -1);
+        break;
+      case SQLITE_BLOB:
+        Tcl_ListObjAppendElement(0, pList, Tcl_NewStringObj("b", 1));
+        pObj = Tcl_NewByteArrayObj(
+            sqlite3_value_blob(pVal),
+            sqlite3_value_bytes(pVal)
+        );
+        break;
+    }
+    Tcl_ListObjAppendElement(0, pList, pObj);
+  }
+}
+
+/*
+** sqlite3session_foreach VARNAME CHANGESET SCRIPT
+*/
+static int test_sqlite3session_foreach(
+  void * clientData,
+  Tcl_Interp *interp,
+  int objc,
+  Tcl_Obj *CONST objv[]
+){
+  void *pChangeSet;
+  int nChangeSet;
+  sqlite3_changeset_iter *pIter;
+  int rc;
+
+  if( objc!=4 ){
+    Tcl_WrongNumArgs(interp, 1, objv, "VARNAME CHANGESET SCRIPT");
+    return TCL_ERROR;
+  }
+
+  pChangeSet = (void *)Tcl_GetByteArrayFromObj(objv[2], &nChangeSet);
+  rc = sqlite3changeset_start(&pIter, nChangeSet, pChangeSet);
+  if( rc!=SQLITE_OK ){
+    return test_session_error(interp, rc);
+  }
+
+  while( SQLITE_ROW==sqlite3changeset_next(pIter) ){
+    int nCol;                     /* Number of columns in table */
+    int op;                       /* SQLITE_INSERT, UPDATE or DELETE */
+    const char *zTab;             /* Name of table change applies to */
+    Tcl_Obj *pVar;                /* Tcl value to set $VARNAME to */
+    Tcl_Obj *pOld;                /* Vector of old.* values */
+    Tcl_Obj *pNew;                /* Vector of new.* values */
+
+    sqlite3changeset_op(pIter, &zTab, &nCol, &op);
+    pVar = Tcl_NewObj();
+    Tcl_ListObjAppendElement(0, pVar, Tcl_NewStringObj(
+          op==SQLITE_INSERT ? "INSERT" :
+          op==SQLITE_UPDATE ? "UPDATE" : 
+          "DELETE", -1
+    ));
+    Tcl_ListObjAppendElement(0, pVar, Tcl_NewStringObj(zTab, -1));
+
+    pOld = Tcl_NewObj();
+    if( op!=SQLITE_INSERT ){
+      int i;
+      for(i=0; i<nCol; i++){
+        sqlite3_value *pVal;
+        sqlite3changeset_old(pIter, i, &pVal);
+        test_append_value(pOld, pVal);
+      }
+    }
+    pNew = Tcl_NewObj();
+    if( op!=SQLITE_DELETE ){
+      int i;
+      for(i=0; i<nCol; i++){
+        sqlite3_value *pVal;
+        sqlite3changeset_new(pIter, i, &pVal);
+        test_append_value(pNew, pVal);
+      }
+    }
+    Tcl_ListObjAppendElement(0, pVar, pOld);
+    Tcl_ListObjAppendElement(0, pVar, pNew);
+
+    Tcl_ObjSetVar2(interp, objv[1], 0, pVar, 0);
+    rc = Tcl_EvalObjEx(interp, objv[3], 0);
+    if( rc!=TCL_OK && rc!=TCL_CONTINUE ){
+      sqlite3changeset_finalize(pIter);
+      return rc==TCL_BREAK ? TCL_OK : rc;
+    }
+  }
+  rc = sqlite3changeset_finalize(pIter);
+  if( rc!=SQLITE_OK ){
+    return test_session_error(interp, rc);
+  }
+
+  return TCL_OK;
+}
+
+int TestSession_Init(Tcl_Interp *interp){
+  Tcl_CreateObjCommand(interp, "sqlite3session", test_sqlite3session, 0, 0);
+  Tcl_CreateObjCommand(
+      interp, "sqlite3session_foreach", test_sqlite3session_foreach, 0, 0
+  );
+  return TCL_OK;
+}
+
+#endif
+
diff --git a/main.mk b/main.mk
index 29374c546fb2e155f49aed3c87c8d024a4aaff9b..2fd5295ac5596708f3f4fb7166abc09fddbee25e 100644 (file)
--- a/main.mk
+++ b/main.mk
@@ -47,6 +47,7 @@
 TCCX =  $(TCC) $(OPTS) -I. -I$(TOP)/src -I$(TOP) 
 TCCX += -I$(TOP)/ext/rtree -I$(TOP)/ext/icu -I$(TOP)/ext/fts3
 TCCX += -I$(TOP)/ext/async
+TCCX += -I$(TOP)/ext/session
 
 # Object files for the SQLite library.
 #
@@ -256,7 +257,7 @@ TESTSRC = \
   $(TOP)/src/test_tclvar.c \
   $(TOP)/src/test_thread.c \
   $(TOP)/src/test_vfs.c \
-  $(TOP)/src/test_wsd.c
+  $(TOP)/src/test_wsd.c 
 
 #TESTSRC += $(TOP)/ext/fts2/fts2_tokenizer.c
 #TESTSRC += $(TOP)/ext/fts3/fts3_tokenizer.c
@@ -298,7 +299,9 @@ TESTSRC2 = \
   $(TOP)/ext/fts3/fts3_expr.c \
   $(TOP)/ext/fts3/fts3_tokenizer.c \
   $(TOP)/ext/fts3/fts3_write.c \
-  $(TOP)/ext/async/sqlite3async.c
+  $(TOP)/ext/async/sqlite3async.c \
+  $(TOP)/ext/session/sqlite3session.c \
+  $(TOP)/ext/session/test_session.c
 
 # Header files used by all library source files.
 #
index 132ab8f06894267192736e48f7f40f6588c1b196..2183d773ea1ae9612909bda02bb421828cd3e571 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Add\sthe\sexperimental\ssqlite3_transaction_hook()\sAPI.
-D 2011-03-03T20:06:00
+C Add\sstart\sof\ssessions\sfeature.
+D 2011-03-08T19:22:51
 F Makefile.arm-wince-mingw32ce-gcc d6df77f1f48d690bd73162294bbba7f59507c72f
 F Makefile.in 27701a1653595a1f2187dc61c8117e00a6c1d50f
 F Makefile.linux-gcc 91d710bdc4998cb015f39edf3cb314ec4f4d7e23
@@ -98,9 +98,12 @@ F ext/rtree/rtree_util.tcl 06aab2ed5b826545bf215fff90ecb9255a8647ea
 F ext/rtree/sqlite3rtree.h 1af0899c63a688e272d69d8e746f24e76f10a3f0
 F ext/rtree/tkt3363.test 142ab96eded44a3615ec79fba98c7bde7d0f96de
 F ext/rtree/viewrtree.tcl eea6224b3553599ae665b239bd827e182b466024
+F ext/session/sqlite3session.c 88d87c48fde70fb03ae61b462527b82af561dda1
+F ext/session/sqlite3session.h b844cae3db05d2db363fd01050efe568b4e3ebec
+F ext/session/test_session.c 72b1af5ea65c91a14faa6524e284e101b6cee6ec
 F install-sh 9d4de14ab9fb0facae2f48780b874848cbf2f895
 F ltmain.sh 3ff0879076df340d2e23ae905484d8c15d5fdea8
-F main.mk 54190fab7cdba523e311c274c95ea480f32abfb5
+F main.mk ae0868e05c76eaa8a0ae3d6927a949b1c8e810d7
 F mkdll.sh 7d09b23c05d56532e9d44a50868eb4b12ff4f74a
 F mkextu.sh 416f9b7089d80e5590a29692c9d9280a10dbad9f
 F mkextw.sh 4123480947681d9b434a5e7b1ee08135abe409ac
@@ -183,7 +186,7 @@ F src/sqliteInt.h 85e635183013e806069320ffa6080a77183f9225
 F src/sqliteLimit.h a17dcd3fb775d63b64a43a55c54cb282f9726f44
 F src/status.c 4997380fbb915426fef9e500b4872e79c99267fc
 F src/table.c 2cd62736f845d82200acfa1287e33feb3c15d62e
-F src/tclsqlite.c 64aeb4109ee2b38fd820b8e8de45b02377fcddc0
+F src/tclsqlite.c b124fb73120d1bc4b78ae8153bce71dbd2d93db5
 F src/test1.c 9020310c7617234b33fd1c3064f89524db25f290
 F src/test2.c 80d323d11e909cf0eb1b6fbb4ac22276483bcf31
 F src/test3.c 056093cfef69ff4227a6bdb9108564dc7f45e4bc
@@ -231,9 +234,9 @@ F src/util.c ab1c92426494f499f42b9e307537b03e923d75c1
 F src/vacuum.c 924bd1bcee2dfb05376f79845bd3b4cec7b54b2f
 F src/vdbe.c 2c523bc17f915329f3db3745fc76ed9bfc5c26bb
 F src/vdbe.h 4de0efb4b0fdaaa900cf419b35c458933ef1c6d2
-F src/vdbeInt.h 41b8e337332f279228196039ab86acd353ad0c6c
-F src/vdbeapi.c ea741433042d95c392b29397ac6c2b08f6106317
-F src/vdbeaux.c cc817d8597307a2be361c24f3b436a7e41fa67dc
+F src/vdbeInt.h 9dd04435bd5a68e30cd07e7a91c17b062bf1c23d
+F src/vdbeapi.c 750dcdea999762adbac6eddde5bd790803911edf
+F src/vdbeaux.c 9ba08ed7de89e923ba6b95a5a75a460e6507a81f
 F src/vdbeblob.c 18955f0ee6b133cd08e1592010cb9a6b11e9984c
 F src/vdbemem.c 0fa2ed786cd207d5b988afef3562a8e663a75b50
 F src/vdbetrace.c 3ba13bc32bdf16d2bdea523245fd16736bed67b5
@@ -636,6 +639,7 @@ F test/selectA.test 06d1032fa9009314c95394f2ca2e60d9f7ae8532
 F test/selectB.test f305cc6660804cb239aab4e2f26b0e288b59958b
 F test/selectC.test f9bf1bc4581b5b8158caa6e4e4f682acb379fb25
 F test/server1.test f5b790d4c0498179151ca8a7715a65a7802c859c
+F test/session1.test eb069ae24c807089cdb42c06f156e6a4bc467e08
 F test/shared.test b9114eaea7e748a3a4c8ff7b9ca806c8f95cef3e
 F test/shared2.test 7f6ad2d857d0f4e5d6a0b9a897b5e56a6b6ea18c
 F test/shared3.test d69bdd5f156580876c5345652d21dc2092e85962
@@ -909,7 +913,7 @@ F tool/speedtest2.tcl ee2149167303ba8e95af97873c575c3e0fab58ff
 F tool/speedtest8.c 2902c46588c40b55661e471d7a86e4dd71a18224
 F tool/speedtest8inst1.c 293327bc76823f473684d589a8160bde1f52c14e
 F tool/vdbe-compress.tcl d70ea6d8a19e3571d7ab8c9b75cba86d1173ff0f
-P 6145d7b89f83500318713779c60f79a7ab2098ba
-R 2617e052d0e8716cc0b428f99022eddb
+P 093d8cd8e2f3a6af5d40cf810e396f4919eb5cef
+R d2860e2d38705a211d32d9cfd0828f6b
 U dan
-Z 24e44e929785c6b1d1f35eb2447fe0e7
+Z 881eaac7f67a95dfac0116385a169f79
index e0c1dabe5a497681d553fcbef49a28c8bbabf9f1..6c229ed51ef5fb3d5250a1fd61f6721d0978bbbb 100644 (file)
@@ -1 +1 @@
-093d8cd8e2f3a6af5d40cf810e396f4919eb5cef
\ No newline at end of file
+269a81a37d7dbdcdec3c2322074916af0fbac91c
\ No newline at end of file
index c7e6fda0687d6b76e363061c655c2e25af63c5dc..1743b40cf69501f2a61bd865d48226828973a7ef 100644 (file)
@@ -3738,6 +3738,9 @@ static void init_all(Tcl_Interp *interp){
     extern int Sqlitequota_Init(Tcl_Interp*);
     extern int Sqlitemultiplex_Init(Tcl_Interp*);
     extern int SqliteSuperlock_Init(Tcl_Interp*);
+#ifdef SQLITE_ENABLE_SESSION
+    extern int TestSession_Init(Tcl_Interp*);
+#endif
 
 #ifdef SQLITE_ENABLE_ZIPVFS
     extern int Zipvfs_Init(Tcl_Interp*);
@@ -3775,6 +3778,9 @@ static void init_all(Tcl_Interp *interp){
     Sqlitequota_Init(interp);
     Sqlitemultiplex_Init(interp);
     SqliteSuperlock_Init(interp);
+#ifdef SQLITE_ENABLE_SESSION
+    TestSession_Init(interp);
+#endif
 
     Tcl_CreateObjCommand(interp,"load_testfixture_extensions",init_all_cmd,0,0);
 
index a764d23665756f63c60fba377a8aa0724118514e..0b4178aacdeae306e896754d1f8bf9ff4985a7c1 100644 (file)
@@ -339,6 +339,7 @@ struct PreUpdate {
   VdbeCursor *pCsr;               /* Cursor to read old values from */
   int op;                         /* One of SQLITE_INSERT, UPDATE, DELETE */
   u8 *aRecord;                    /* old.* database record */
+  KeyInfo keyinfo;
   UnpackedRecord *pUnpacked;      /* Unpacked version of aRecord[] */
 };
 
index cefca02a24783348a2421af0c47bc5f30d6ae519..d3bd6478cf422223b719d6107a8a44bfb8d9cec8 100644 (file)
@@ -1351,15 +1351,9 @@ int sqlite3_preupdate_old(sqlite3 *db, int iIdx, sqlite3_value **ppValue){
   }
 
   if( p->pUnpacked==0 ){
-    KeyInfo keyinfo;
     u32 nRecord;
     u8 *aRecord;
 
-    memset(&keyinfo, 0, sizeof(KeyInfo));
-    keyinfo.db = db;
-    keyinfo.enc = ENC(db);
-    keyinfo.nField = p->pCsr->nField;
-
     rc = sqlite3BtreeDataSize(p->pCsr->pCursor, &nRecord);
     if( rc!=SQLITE_OK ) goto preupdate_old_out;
     aRecord = sqlite3DbMallocRaw(db, nRecord);
@@ -1370,7 +1364,7 @@ int sqlite3_preupdate_old(sqlite3 *db, int iIdx, sqlite3_value **ppValue){
       goto preupdate_old_out;
     }
 
-    p->pUnpacked = sqlite3VdbeRecordUnpack(&keyinfo, nRecord, aRecord, 0, 0);
+    p->pUnpacked = sqlite3VdbeRecordUnpack(&p->keyinfo, nRecord, aRecord, 0, 0);
     p->aRecord = aRecord;
   }
 
index 937c2a1998703f73b8d9fa50beba91e947d10681..cb6aefac08b79b92ba207ec617ab081241be523b 100644 (file)
@@ -3194,6 +3194,9 @@ void sqlite3VdbePreUpdateHook(
 
   preupdate.pCsr = pCsr;
   preupdate.op = op;
+  preupdate.keyinfo.db = db;
+  preupdate.keyinfo.enc = ENC(db);
+  preupdate.keyinfo.nField = pCsr->nField;
   db->pPreUpdate = &preupdate;
   db->xPreUpdateCallback(db->pPreUpdateArg, db, op, zDb, zTbl, iKey1, iKey2);
   db->pPreUpdate = 0;
diff --git a/test/session1.test b/test/session1.test
new file mode 100644 (file)
index 0000000..0cb225a
--- /dev/null
@@ -0,0 +1,118 @@
+# 2011 March 07
+#
+# 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.
+#
+
+set testdir [file dirname $argv0]
+source $testdir/tester.tcl
+set ::testprefix session1
+
+do_execsql_test 1.0 {
+  CREATE TABLE t1(x, y);
+  INSERT INTO t1 VALUES('abc', 'def');
+}
+
+do_test 1.1 { sqlite3session S db main } {S}
+do_test 1.2 { S delete } {}
+do_test 1.3 { sqlite3session S db main } {S}
+do_test 1.4 { S attach t1 } {}
+do_test 1.5 { S delete } {}
+do_test 1.6 { sqlite3session S db main } {S}
+do_test 1.7 { S attach t1 ; S attach t2 ; S attach t3 } {}
+do_test 1.8 { S attach t1 ; S attach t2 ; S attach t3 } {}
+do_test 1.9 { S delete } {}
+do_test 1.10 {
+  sqlite3session S db main
+  S attach t1
+  execsql { INSERT INTO t1 VALUES('ghi', 'jkl') }
+} {}
+do_test 1.11 { S delete } {}
+do_test 1.12 {
+  sqlite3session S db main
+  S attach t1
+  execsql { INSERT INTO t1 VALUES('mno', 'pqr') }
+  execsql { UPDATE t1 SET x = 111 WHERE rowid = 1 }
+  execsql { DELETE FROM t1 WHERE rowid = 2 }
+} {}
+do_test 1.13 {
+  S changeset
+  S delete
+} {}
+
+proc do_changeset_test {tn session res} {
+  set r [list]
+  foreach x $res {lappend r $x}
+  uplevel do_test $tn [list [subst -nocommands {
+    set x [list]
+    sqlite3session_foreach c [$session changeset] { lappend x [set c] }
+    set x
+  }]] [list $r]
+}
+
+do_test 2.1.1 {
+  execsql { DELETE FROM t1 }
+  sqlite3session S db main
+  S attach t1
+  execsql { INSERT INTO t1 VALUES(1, 'Sukhothai') }
+  execsql { INSERT INTO t1 VALUES(2, 'Ayutthaya') }
+  execsql { INSERT INTO t1 VALUES(3, 'Thonburi') }
+} {}
+do_changeset_test 2.1.2 S {
+  {INSERT t1 {} {i 1 t Sukhothai}}
+  {INSERT t1 {} {i 2 t Ayutthaya}}
+  {INSERT t1 {} {i 3 t Thonburi}}
+}
+do_test 2.1.3 { S delete } {}
+
+do_test 2.2.1 {
+  sqlite3session S db main
+  S attach t1
+  execsql { DELETE FROM t1 WHERE 1 }
+} {}
+do_changeset_test 2.2.2 S {
+  {DELETE t1 {i 1 t Sukhothai} {}}
+  {DELETE t1 {i 2 t Ayutthaya} {}}
+  {DELETE t1 {i 3 t Thonburi} {}}
+}
+do_test 2.2.3 { S delete } {}
+
+do_test 2.3.1 {
+  sqlite3session S db main
+  execsql { INSERT INTO t1 VALUES(1, 'Sukhothai') }
+  execsql { INSERT INTO t1 VALUES(2, 'Ayutthaya') }
+  execsql { INSERT INTO t1 VALUES(3, 'Thonburi') }
+  S attach t1
+  execsql { 
+    UPDATE t1 SET x = 10 WHERE x = 1;
+    UPDATE t1 SET y = 'Surin' WHERE x = 2;
+    UPDATE t1 SET x = 20, y = 'Thapae' WHERE x = 3;
+  }
+} {}
+
+do_changeset_test 2.3.2 S {
+  {UPDATE t1 {i 1 {} {}} {i 10 {} {}}} 
+  {UPDATE t1 {{} {} t Ayutthaya} {{} {} t Surin}} 
+  {UPDATE t1 {i 3 t Thonburi} {i 20 t Thapae}}
+}
+do_test 2.3.3 { S delete } {}
+
+do_test 2.4.1 {
+  sqlite3session S db main
+  S attach t1
+  execsql { INSERT INTO t1 VALUES(100, 'Bangkok') }
+  execsql { DELETE FROM t1 WHERE x = 100 }
+} {}
+
+breakpoint
+do_changeset_test 2.4.2 S {}
+do_test 2.4.3 { S delete } {}
+
+finish_test