]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Add an experimental module to detect conflicts between sessions changesets.
authordan <dan@noemail.net>
Mon, 22 Aug 2016 20:49:06 +0000 (20:49 +0000)
committerdan <dan@noemail.net>
Mon, 22 Aug 2016 20:49:06 +0000 (20:49 +0000)
FossilOrigin-Name: 0c9fd6b723041955b5182caa430312e5124fdc83

ext/session/changebatch1.test [new file with mode: 0644]
ext/session/sqlite3changebatch.c [new file with mode: 0644]
ext/session/sqlite3changebatch.h [new file with mode: 0644]
ext/session/test_session.c
main.mk
manifest
manifest.uuid

diff --git a/ext/session/changebatch1.test b/ext/session/changebatch1.test
new file mode 100644 (file)
index 0000000..6e9d1a9
--- /dev/null
@@ -0,0 +1,55 @@
+# 2016 August 23
+#
+# 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.
+#
+
+if {![info exists testdir]} {
+  set testdir [file join [file dirname [info script]] .. .. test]
+} 
+source $testdir/tester.tcl
+ifcapable !session {finish_test; return}
+
+set testprefix changebatch1
+
+proc do_changebatch_test {tn args} {
+  set C [list]
+  foreach a $args {
+    sqlite3session S db main
+    S attach *
+    execsql $a
+    lappend C [S changeset]
+    S delete
+  }
+
+  sqlite3changebatch cb db
+  set i 1
+  foreach ::cs [lrange $C 0 end-1] {
+    do_test $tn.$i { cb add [set ::cs] } SQLITE_OK
+    incr i
+  }
+
+  set ::cs [lindex $C end]
+  do_test $tn.$i { cb add [set ::cs] } SQLITE_CONSTRAINT
+
+  cb delete
+}
+
+do_execsql_test 1.0 {
+  CREATE TABLE t1(a PRIMARY KEY, b);
+}
+
+do_changebatch_test 1.1 {
+  INSERT INTO t1 VALUES(1, 1);
+} {
+  DELETE FROM t1 WHERE a=1;
+}
+
+finish_test
diff --git a/ext/session/sqlite3changebatch.c b/ext/session/sqlite3changebatch.c
new file mode 100644 (file)
index 0000000..c0f9b28
--- /dev/null
@@ -0,0 +1,442 @@
+
+#if !defined(SQLITE_TEST) || (defined(SQLITE_ENABLE_SESSION) && defined(SQLITE_ENABLE_PREUPDATE_HOOK))
+
+#include "sqlite3session.h"
+#include "sqlite3changebatch.h"
+
+#include <assert.h>
+#include <string.h>
+
+typedef struct BatchTable BatchTable;
+typedef struct BatchIndex BatchIndex;
+typedef struct BatchIndexEntry BatchIndexEntry;
+typedef struct BatchHash BatchHash;
+
+struct sqlite3_changebatch {
+  sqlite3 *db;                    /* Database handle used to read schema */
+  BatchTable *pTab;               /* First in linked list of tables */
+  int iChangesetId;               /* Current changeset id */
+  int iNextIdxId;                 /* Next available index id */
+  int nEntry;                     /* Number of entries in hash table */
+  int nHash;                      /* Number of hash buckets */
+  BatchIndexEntry **apHash;       /* Array of hash buckets */
+};
+
+struct BatchTable {
+  BatchIndex *pIdx;               /* First in linked list of UNIQUE indexes */
+  BatchTable *pNext;              /* Next table */
+  char zTab[1];                   /* Table name */
+};
+
+struct BatchIndex {
+  BatchIndex *pNext;              /* Next index on same table */
+  int iId;                        /* Index id (assigned internally) */
+  int bPk;                        /* True for PK index */
+  int nCol;                       /* Size of aiCol[] array */
+  int *aiCol;                     /* Array of columns that make up index */
+};
+
+struct BatchIndexEntry {
+  BatchIndexEntry *pNext;         /* Next colliding hash table entry */
+  int iChangesetId;               /* Id of associated changeset */
+  int iIdxId;                     /* Id of index this key is from */
+  int szRecord;
+  char aRecord[1];
+};
+
+/*
+** Allocate and zero a block of nByte bytes. Must be freed using cbFree().
+*/
+static void *cbMalloc(int *pRc, int nByte){
+  void *pRet;
+
+  if( *pRc ){
+    pRet = 0;
+  }else{
+    pRet = sqlite3_malloc(nByte);
+    if( pRet ){
+      memset(pRet, 0, nByte);
+    }else{
+      *pRc = SQLITE_NOMEM;
+    }
+  }
+
+  return pRet;
+}
+
+/*
+** Free an allocation made by cbMalloc().
+*/
+static void cbFree(void *p){
+  sqlite3_free(p);
+}
+
+/*
+** Return the hash bucket that pEntry belongs in.
+*/
+static int cbHash(sqlite3_changebatch *p, BatchIndexEntry *pEntry){
+  unsigned int iHash = (unsigned int)pEntry->iIdxId;
+  unsigned char *pEnd = (unsigned char*)&pEntry->aRecord[pEntry->szRecord];
+  unsigned char *pIter;
+
+  for(pIter=pEntry->aRecord; pIter<pEnd; pIter++){
+    iHash += (iHash << 7) + *pIter;
+  }
+
+  return (int)(iHash % p->nHash);
+}
+
+/*
+** Resize the hash table.
+*/
+static int cbHashResize(sqlite3_changebatch *p){
+  int rc = SQLITE_OK;
+  BatchIndexEntry **apNew;
+  int nNew = (p->nHash ? p->nHash*2 : 512);
+  int i;
+
+  apNew = cbMalloc(&rc, sizeof(BatchIndexEntry*) * nNew);
+  if( rc==SQLITE_OK ){
+    int nHash = p->nHash;
+    p->nHash = nNew;
+    for(i=0; i<nHash; i++){
+      BatchIndexEntry *pEntry;
+      while( pEntry=p->apHash[i] ){
+        int iHash = cbHash(p, pEntry);
+        p->apHash[i] = pEntry->pNext;
+        pEntry->pNext = apNew[iHash];
+        apNew[iHash] = pEntry;
+      }
+    }
+
+    cbFree(p->apHash);
+    p->apHash = apNew;
+  }
+
+  return rc;
+}
+
+
+/*
+** Allocate a new sqlite3_changebatch object.
+*/
+int sqlite3changebatch_new(sqlite3 *db, sqlite3_changebatch **pp){
+  sqlite3_changebatch *pRet;
+  int rc = SQLITE_OK;
+  *pp = pRet = (sqlite3_changebatch*)cbMalloc(&rc, sizeof(sqlite3_changebatch));
+  if( pRet ){
+    pRet->db = db;
+  }
+  return rc;
+}
+
+/*
+** Add a BatchIndex entry for index zIdx to table pTab.
+*/
+static int cbAddIndex(
+  sqlite3_changebatch *p, 
+  BatchTable *pTab, 
+  const char *zIdx, 
+  int bPk
+){
+  int nCol = 0;
+  sqlite3_stmt *pIndexInfo = 0;
+  BatchIndex *pNew = 0;
+  int rc;
+  char *zIndexInfo;
+
+  zIndexInfo = (char*)sqlite3_mprintf("PRAGMA main.index_info = %Q", zIdx);
+  if( zIndexInfo ){
+    rc = sqlite3_prepare_v2(p->db, zIndexInfo, -1, &pIndexInfo, 0);
+    sqlite3_free(zIndexInfo);
+  }else{
+    rc = SQLITE_NOMEM;
+  }
+
+  if( rc==SQLITE_OK ){
+    int rc2;
+    while( SQLITE_ROW==sqlite3_step(pIndexInfo) ){ nCol++; }
+    rc2 = sqlite3_reset(pIndexInfo);
+    if( rc==SQLITE_OK ) rc = rc2;
+  }
+
+  pNew = (BatchIndex*)cbMalloc(&rc, sizeof(BatchIndex) + sizeof(int) * nCol);
+  if( rc==SQLITE_OK ){
+    int rc2;
+    pNew->nCol = nCol;
+    pNew->bPk = bPk;
+    pNew->aiCol = (int*)&pNew[1];
+    pNew->iId = p->iNextIdxId++;
+    while( SQLITE_ROW==sqlite3_step(pIndexInfo) ){ 
+      int i = sqlite3_column_int(pIndexInfo, 0);
+      int j = sqlite3_column_int(pIndexInfo, 1);
+      pNew->aiCol[i] = j;
+    }
+    rc2 = sqlite3_reset(pIndexInfo);
+    if( rc==SQLITE_OK ) rc = rc2;
+  }
+
+  if( rc==SQLITE_OK ){
+    pNew->pNext = pTab->pIdx;
+    pTab->pIdx = pNew;
+  }else{
+    cbFree(pNew);
+  }
+  sqlite3_finalize(pIndexInfo);
+
+  return rc;
+}
+
+/*
+** Find or create the BatchTable object named zTab.
+*/
+static int cbFindTable(
+  sqlite3_changebatch *p, 
+  const char *zTab, 
+  BatchTable **ppTab
+){
+  BatchTable *pRet = 0;
+  int rc = SQLITE_OK;
+
+  for(pRet=p->pTab; pRet; pRet=pRet->pNext){
+    if( 0==sqlite3_stricmp(zTab, pRet->zTab) ) break;
+  }
+
+  if( pRet==0 ){
+    int nTab = strlen(zTab);
+    pRet = (BatchTable*)cbMalloc(&rc, nTab + sizeof(BatchTable));
+    if( pRet ){
+      sqlite3_stmt *pIndexList = 0;
+      char *zIndexList = 0;
+      int rc2;
+      memcpy(pRet->zTab, zTab, nTab);
+
+      zIndexList = sqlite3_mprintf("PRAGMA main.index_list = %Q", zTab);
+      if( zIndexList==0 ){
+        rc = SQLITE_NOMEM;
+      }else{
+        rc = sqlite3_prepare_v2(p->db, zIndexList, -1, &pIndexList, 0);
+        sqlite3_free(zIndexList);
+      }
+
+      while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pIndexList) ){
+        if( sqlite3_column_int(pIndexList, 2) ){
+          const char *zIdx = (const char*)sqlite3_column_text(pIndexList, 1);
+          const char *zTyp = (const char*)sqlite3_column_text(pIndexList, 3);
+          rc = cbAddIndex(p, pRet, zIdx, (zTyp[0]=='p'));
+        }
+      }
+      rc2 = sqlite3_finalize(pIndexList);
+      if( rc==SQLITE_OK ) rc = rc2;
+
+      if( rc==SQLITE_OK ){
+        pRet->pNext = p->pTab;
+        p->pTab = pRet;
+      }
+    }
+  }
+
+  *ppTab = pRet;
+  return rc;
+}
+
+static int cbAddToHash(
+  sqlite3_changebatch *p, 
+  sqlite3_changeset_iter *pIter, 
+  BatchIndex *pIdx, 
+  int (*xVal)(sqlite3_changeset_iter*,int,sqlite3_value**),
+  int *pbConf
+){
+  BatchIndexEntry *pNew;
+  int sz = pIdx->nCol;
+  int i;
+  int iOut = 0;
+  int rc = SQLITE_OK;
+
+  for(i=0; rc==SQLITE_OK && i<pIdx->nCol; i++){
+    sqlite3_value *pVal;
+    rc = xVal(pIter, pIdx->aiCol[i], &pVal);
+    if( rc==SQLITE_OK ){
+      int eType = 0;
+      if( pVal ){
+        eType = sqlite3_value_type(pVal);
+      }
+      switch( eType ){
+        case 0:
+        case SQLITE_NULL:
+          return SQLITE_OK;
+
+        case SQLITE_INTEGER:
+          sz += 8;
+          break;
+        case SQLITE_FLOAT:
+          sz += 8;
+          break;
+
+        default:
+          assert( eType==SQLITE_TEXT || eType==SQLITE_BLOB );
+          sz += sqlite3_value_bytes(pVal);
+          break;
+      }
+    }
+  }
+
+  pNew = cbMalloc(&rc, sizeof(BatchIndexEntry) + sz);
+  if( pNew ){
+    pNew->iChangesetId = p->iChangesetId;
+    pNew->iIdxId = pIdx->iId;
+    pNew->szRecord = sz;
+
+    for(i=0; rc==SQLITE_OK && i<pIdx->nCol; i++){
+      sqlite3_value *pVal;
+      rc = xVal(pIter, pIdx->aiCol[i], &pVal);
+      if( rc==SQLITE_OK ){
+        int eType = sqlite3_value_type(pVal);
+        pNew->aRecord[iOut++] = eType;
+        switch( eType ){
+          case SQLITE_INTEGER: {
+            sqlite3_int64 i64 = sqlite3_value_int64(pVal);
+            memcpy(&pNew->aRecord[iOut], &i64, 8);
+            iOut += 8;
+            break;
+          }
+          case SQLITE_FLOAT: {
+            double d64 = sqlite3_value_double(pVal);
+            memcpy(&pNew->aRecord[iOut], &d64, sizeof(double));
+            iOut += sizeof(double);
+            break;
+          }
+
+          default: {
+            int nByte = sqlite3_value_bytes(pVal);
+            const char *z = (const char*)sqlite3_value_blob(pVal);
+            memcpy(&pNew->aRecord[iOut], z, nByte);
+            iOut += nByte;
+            break;
+          }
+        }
+      }
+    }
+  }
+
+  if( rc==SQLITE_OK && p->nEntry>=(p->nHash/2) ){
+    rc = cbHashResize(p);
+  }
+
+  if( rc==SQLITE_OK ){
+    BatchIndexEntry *pIter;
+    int iHash = cbHash(p, pNew);
+
+    assert( iHash>=0 && iHash<p->nHash );
+    for(pIter=p->apHash[iHash]; pIter; pIter=pIter->pNext){
+      if( pNew->szRecord==pIter->szRecord 
+       && 0==memcmp(pNew->aRecord, pIter->aRecord, pNew->szRecord)
+      ){
+        if( pNew->iChangesetId!=pIter->iChangesetId ){
+          *pbConf = 1;
+        }
+        cbFree(pNew);
+        pNew = 0;
+        break;
+      }
+    }
+
+    if( pNew ){
+      pNew->pNext = p->apHash[iHash];
+      p->apHash[iHash] = pNew;
+      p->nEntry++;
+    }
+  }
+
+  p->iChangesetId++;
+  return rc;
+}
+
+
+/*
+** Add a changeset to the current batch.
+*/
+int sqlite3changebatch_add(sqlite3_changebatch *p, void *pBuf, int nBuf){
+  sqlite3_changeset_iter *pIter;  /* Iterator opened on pBuf/nBuf */
+  int rc;                         /* Return code */
+  int bConf = 0;                  /* Conflict was detected */
+
+  rc = sqlite3changeset_start(&pIter, nBuf, pBuf);
+  if( rc==SQLITE_OK ){
+    int rc2;
+    for(rc2 = sqlite3changeset_next(pIter);
+        rc2==SQLITE_ROW;
+        rc2 = sqlite3changeset_next(pIter)
+    ){
+      BatchTable *pTab;
+      BatchIndex *pIdx;
+      const char *zTab;           /* Table this change applies to */
+      int nCol;                   /* Number of columns in table */
+      int op;                     /* UPDATE, INSERT or DELETE */
+
+      sqlite3changeset_op(pIter, &zTab, &nCol, &op, 0);
+      assert( op==SQLITE_INSERT || op==SQLITE_UPDATE || op==SQLITE_DELETE );
+
+      rc = cbFindTable(p, zTab, &pTab);
+      for(pIdx=pTab->pIdx; pIdx && rc==SQLITE_OK; pIdx=pIdx->pNext){
+        if( op==SQLITE_UPDATE && pIdx->bPk ) continue;
+        if( op==SQLITE_UPDATE || op==SQLITE_DELETE ){
+          rc = cbAddToHash(p, pIter, pIdx, sqlite3changeset_old, &bConf);
+        }
+        if( op==SQLITE_UPDATE || op==SQLITE_INSERT ){
+          rc = cbAddToHash(p, pIter, pIdx, sqlite3changeset_new, &bConf);
+        }
+      }
+      if( rc!=SQLITE_OK ) break;
+    }
+
+    rc2 = sqlite3changeset_finalize(pIter);
+    if( rc==SQLITE_OK ) rc = rc2;
+  }
+
+  if( rc==SQLITE_OK && bConf ){
+    rc = SQLITE_CONSTRAINT;
+  }
+  return rc;
+}
+
+/*
+** Zero an existing changebatch object.
+*/
+void sqlite3changebatch_zero(sqlite3_changebatch *p){
+  int i;
+  for(i=0; i<p->nHash; i++){
+    BatchIndexEntry *pEntry;
+    BatchIndexEntry *pNext;
+    for(pEntry=p->apHash[i]; pEntry; pEntry=pNext){
+      pNext = pEntry->pNext;
+      cbFree(pEntry);
+    }
+  }
+  cbFree(p->apHash);
+  p->nHash = 0;
+  p->apHash = 0;
+}
+
+/*
+** Delete a changebatch object.
+*/
+void sqlite3changebatch_delete(sqlite3_changebatch *p){
+  BatchTable *pTab;
+  BatchTable *pTabNext;
+
+  sqlite3changebatch_zero(p);
+  for(pTab=p->pTab; pTab; pTab=pTabNext){
+    BatchIndex *pIdx;
+    BatchIndex *pIdxNext;
+    for(pIdx=pTab->pIdx; pIdx; pIdx=pIdxNext){
+      pIdxNext = pIdx->pNext;
+      cbFree(pIdx);
+    }
+    pTabNext = pTab->pNext;
+    cbFree(pTab);
+  }
+  cbFree(p);
+}
+
+#endif /* SQLITE_ENABLE_SESSION && SQLITE_ENABLE_PREUPDATE_HOOK */
diff --git a/ext/session/sqlite3changebatch.h b/ext/session/sqlite3changebatch.h
new file mode 100644 (file)
index 0000000..9afa6c1
--- /dev/null
@@ -0,0 +1,75 @@
+
+#if !defined(SQLITECHANGEBATCH_H_) 
+#define SQLITECHANGEBATCH_H_ 1
+
+typedef struct sqlite3_changebatch sqlite3_changebatch;
+
+/*
+** Create a new changebatch object for detecting conflicts between
+** changesets associated with a schema equivalent to that of the "main"
+** database of the open database handle db passed as the first
+** parameter. It is the responsibility of the caller to ensure that
+** the database handle is not closed until after the changebatch
+** object has been deleted.
+**
+** A changebatch object is used to detect batches of non-conflicting
+** changesets. Changesets that do not conflict may be applied to the 
+** target database in any order without affecting the final state of 
+** the database.
+**
+** The changebatch object only works reliably if PRIMARY KEY and UNIQUE
+** constraints on tables affected by the changesets use collation
+** sequences that are equivalent to built-in collation sequence 
+** BINARY for the == operation.
+**
+** If successful, SQLITE_OK is returned and (*pp) set to point to
+** the new changebatch object. If an error occurs, an SQLite error
+** code is returned and the final value of (*pp) is undefined.
+*/
+int sqlite3changebatch_new(sqlite3 *db, sqlite3_changebatch **pp);
+
+/*
+** Argument p points to a buffer containing a changeset n bytes in
+** size. Assuming no error occurs, this function returns SQLITE_OK
+** if the changeset does not conflict with any changeset passed 
+** to an sqlite3changebatch_add() call made on the same 
+** sqlite3_changebatch* handle since the most recent call to
+** sqlite3changebatch_zero(). If the changeset does conflict with 
+** an earlier such changeset, SQLITE_CONSTRAINT is returned. Or, 
+** if an error occurs, some other SQLite error code may be returned.
+**
+** One changeset is said to conflict with another if
+** either:
+**
+**   * the two changesets contain operations (INSERT, UPDATE or 
+**     DELETE) on the same row, identified by primary key, or
+**
+**   * the two changesets contain operations (INSERT, UPDATE or 
+**     DELETE) on rows with identical values in any combination 
+**     of fields constrained by a UNIQUE constraint.
+**
+** Even if this function returns SQLITE_CONFLICT, the current
+** changeset is added to the internal data structures - so future
+** calls to this function may conflict with it. If this function
+** returns any result code other than SQLITE_OK or SQLITE_CONFLICT,
+** the result of any future call to sqlite3changebatch_add() is
+** undefined.
+**
+** Only changesets may be passed to this function. Passing a 
+** patchset to this function results in an SQLITE_MISUSE error.
+*/
+int sqlite3changebatch_add(sqlite3_changebatch*, void *p, int n);
+
+/*
+** Zero a changebatch object. This causes the records of all earlier 
+** calls to sqlite3changebatch_add() to be discarded.
+*/
+void sqlite3changebatch_zero(sqlite3_changebatch*);
+
+/*
+** Delete a changebatch object.
+*/
+void sqlite3changebatch_delete(sqlite3_changebatch*);
+
+#endif /* !defined(SQLITECHANGEBATCH_H_) */
+
index 103a1c2d7a91f7138c8b52b7f6f6c43d944bfe23..3f4cdd3c34f4a065c98ba88fecf9513352119e70 100644 (file)
@@ -373,7 +373,7 @@ static int test_filter_handler(
 
   Tcl_DecrRefCount(pEval);
   return res;
-}  
+}
 
 static int test_conflict_handler(
   void *pCtx,                     /* Pointer to TestConflictHandler structure */
@@ -918,6 +918,127 @@ static int SQLITE_TCLAPI test_sqlite3session_foreach(
   return TCL_OK;
 }
 
+#include "sqlite3changebatch.h"
+
+typedef struct TestChangebatch TestChangebatch;
+struct TestChangebatch {
+  sqlite3_changebatch *pChangebatch;
+};
+
+/*
+** Tclcmd:  $changebatch add BLOB
+**          $changebatch zero
+**          $changebatch delete
+*/
+static int SQLITE_TCLAPI test_changebatch_cmd(
+  void *clientData,
+  Tcl_Interp *interp,
+  int objc,
+  Tcl_Obj *CONST objv[]
+){
+  TestChangebatch *p = (TestChangebatch*)clientData;
+  sqlite3_changebatch *pChangebatch = p->pChangebatch;
+  struct SessionSubcmd {
+    const char *zSub;
+    int nArg;
+    const char *zMsg;
+    int iSub;
+  } aSub[] = {
+    { "add",          1, "CHANGESET",  }, /* 0 */
+    { "zero",         0, "",           }, /* 1 */
+    { "delete",       0, "",           }, /* 2 */
+    { 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: {      /* add */
+      int nArg;
+      unsigned char *pArg = Tcl_GetByteArrayFromObj(objv[2], &nArg);
+      rc = sqlite3changebatch_add(pChangebatch, pArg, nArg);
+      if( rc!=SQLITE_OK && rc!=SQLITE_CONSTRAINT ){
+        return test_session_error(interp, rc, 0);
+      }else{
+        extern const char *sqlite3ErrName(int);
+        Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1));
+      }
+      break;
+    }
+
+    case 1: {      /* zero */
+      sqlite3changebatch_zero(pChangebatch);
+      break;
+    }
+
+    case 2:        /* delete */
+      Tcl_DeleteCommand(interp, Tcl_GetString(objv[0]));
+      break;
+  }
+
+  return TCL_OK;
+}
+
+static void SQLITE_TCLAPI test_changebatch_del(void *clientData){
+  TestChangebatch *p = (TestChangebatch*)clientData;
+  sqlite3changebatch_delete(p->pChangebatch);
+  ckfree((char*)p);
+}
+
+/*
+** Tclcmd:  sqlite3changebatch CMD DB-HANDLE
+*/
+static int SQLITE_TCLAPI test_sqlite3changebatch(
+  void * clientData,
+  Tcl_Interp *interp,
+  int objc,
+  Tcl_Obj *CONST objv[]
+){
+  sqlite3 *db;
+  Tcl_CmdInfo info;
+  int rc;                         /* sqlite3session_create() return code */
+  TestChangebatch *p;             /* New wrapper object */
+
+  if( objc!=3 ){
+    Tcl_WrongNumArgs(interp, 1, objv, "CMD DB-HANDLE");
+    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;
+
+  p = (TestChangebatch*)ckalloc(sizeof(TestChangebatch));
+  memset(p, 0, sizeof(TestChangebatch));
+  rc = sqlite3changebatch_new(db, &p->pChangebatch);
+  if( rc!=SQLITE_OK ){
+    ckfree((char*)p);
+    return test_session_error(interp, rc, 0);
+  }
+
+  Tcl_CreateObjCommand(
+      interp, Tcl_GetString(objv[1]), test_changebatch_cmd, (ClientData)p,
+      test_changebatch_del
+  );
+  Tcl_SetObjResult(interp, objv[1]);
+  return TCL_OK;
+}
+
 int TestSession_Init(Tcl_Interp *interp){
   Tcl_CreateObjCommand(interp, "sqlite3session", test_sqlite3session, 0, 0);
   Tcl_CreateObjCommand(
@@ -936,6 +1057,10 @@ int TestSession_Init(Tcl_Interp *interp){
       interp, "sqlite3changeset_apply_replace_all", 
       test_sqlite3changeset_apply_replace_all, 0, 0
   );
+
+  Tcl_CreateObjCommand(
+      interp, "sqlite3changebatch", test_sqlite3changebatch, 0, 0
+  );
   return TCL_OK;
 }
 
diff --git a/main.mk b/main.mk
index 451837ffff98cb86592fb23d4082547c7829c5f2..034b65332a2c54d48b8e06e2eedeb958b9a9107e 100644 (file)
--- a/main.mk
+++ b/main.mk
@@ -390,6 +390,7 @@ TESTSRC2 = \
   $(TOP)/ext/fts3/fts3_write.c \
   $(TOP)/ext/async/sqlite3async.c \
   $(TOP)/ext/session/sqlite3session.c \
+  $(TOP)/ext/session/sqlite3changebatch.c \
   $(TOP)/ext/session/test_session.c 
 
 # Header files used by all library source files.
index 2c686caa9b5d42b8978a638a80ddb5b7d5cce7dc..698acd2427ebe6d5d24c251aa746e99560fb1efc 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Enhance\sthe\sVACUUM\scommand\sso\sthat\sit\scan\soperate\son\san\sattached\sdatabase.
-D 2016-08-19T15:15:55.612
+C Add\san\sexperimental\smodule\sto\sdetect\sconflicts\sbetween\ssessions\schangesets.
+D 2016-08-22T20:49:06.286
 F Makefile.in cfd8fb987cd7a6af046daa87daa146d5aad0e088
 F Makefile.linux-gcc 7bc79876b875010e8c8f9502eb935ca92aa3c434
 F Makefile.msc d66d0395c38571aab3804f8db0fa20707ae4609a
@@ -281,6 +281,7 @@ F ext/rtree/rtree_util.tcl 06aab2ed5b826545bf215fff90ecb9255a8647ea
 F ext/rtree/sqlite3rtree.h 9c5777af3d2921c7b4ae4954e8e5697502289d28
 F ext/rtree/tkt3363.test 142ab96eded44a3615ec79fba98c7bde7d0f96de
 F ext/rtree/viewrtree.tcl eea6224b3553599ae665b239bd827e182b466024
+F ext/session/changebatch1.test 5a69388b91ad02544ef2d9ae27771dbf0f9cea58
 F ext/session/changeset.c 4ccbaa4531944c24584bf6a61ba3a39c62b6267a
 F ext/session/session1.test 98f384736e2bc21ccf5ed81bdadcff4ad863393b
 F ext/session/session2.test 284de45abae4cc1082bc52012ee81521d5ac58e0
@@ -300,16 +301,18 @@ F ext/session/sessionG.test 01ef705096a9d3984eebdcca79807a211dee1b60
 F ext/session/session_common.tcl a1293167d14774b5e728836720497f40fe4ea596
 F ext/session/sessionfault.test da273f2712b6411e85e71465a1733b8501dbf6f7
 F ext/session/sessionfault2.test 04aa0bc9aa70ea43d8de82c4f648db4de1e990b0
+F ext/session/sqlite3changebatch.c 37fb1c87d7b3ccaff194411ff8344d9cc056bd98
+F ext/session/sqlite3changebatch.h 50a302e4fc535324309607b13a1993bca074758b
 F ext/session/sqlite3session.c 37485891b4add26cf61495df193c419f36556a32
 F ext/session/sqlite3session.h 69bf73cfd71e58f2ae5d2aa935b2c1a541aee555
-F ext/session/test_session.c 2caed9a659586428c63ca46e4900347b374487d4
+F ext/session/test_session.c 24968972a5709d2ccf5d570f1a13ce009fbc0d86
 F ext/userauth/sqlite3userauth.h 19cb6f0e31316d0ee4afdfb7a85ef9da3333a220
 F ext/userauth/user-auth.txt e6641021a9210364665fe625d067617d03f27b04
 F ext/userauth/userauth.c 5fa3bdb492f481bbc1709fc83c91ebd13460c69e
 F install-sh 9d4de14ab9fb0facae2f48780b874848cbf2f895 x
 F ltmain.sh 3ff0879076df340d2e23ae905484d8c15d5fdea8
 F magic.txt 8273bf49ba3b0c8559cb2774495390c31fd61c60
-F main.mk 1883ecab643b136e8ab3fdc33785e6ea8b5ceb46
+F main.mk de9447348ea580282aa47dbffd20b042bfbec4e1
 F mkso.sh fd21c06b063bb16a5d25deea1752c2da6ac3ed83
 F mptest/config01.test 3c6adcbc50b991866855f1977ff172eb6d901271
 F mptest/config02.test 4415dfe36c48785f751e16e32c20b077c28ae504
@@ -1511,8 +1514,10 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93
 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
 F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
 F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
-P cb9865e14db1c0076618f13400151112f84960cb ad35ef116296e5d6aaeb9ef260bf35bee3bd6d20
-R abd29f9f3d87c2af53d085425233b2a6
-T +closed ad35ef116296e5d6aaeb9ef260bf35bee3bd6d20
-U drh
-Z a6de375e7bb722769082d6e640b9c49b
+P 083f9e6270fa4faa402b91231271da4f3915c79f
+R c89955dc116a299d44f6360524b79a8a
+T *branch * changebatch
+T *sym-changebatch *
+T -sym-trunk *
+U dan
+Z f9a59b56ef4c3ca7ba5584bf19e5e811
index 4e9d48c81e556bd04a4d5b82df5a69b78f2bbe75..7935283ab4e01b2edc12d7d6fa5a818582e13242 100644 (file)
@@ -1 +1 @@
-083f9e6270fa4faa402b91231271da4f3915c79f
\ No newline at end of file
+0c9fd6b723041955b5182caa430312e5124fdc83
\ No newline at end of file