]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Add an API to the sessions module to add changes one at a time to an sqlite3_changegr...
authordan <Dan Kennedy>
Mon, 12 Jan 2026 16:39:27 +0000 (16:39 +0000)
committerdan <Dan Kennedy>
Mon, 12 Jan 2026 16:39:27 +0000 (16:39 +0000)
FossilOrigin-Name: 27150a8c22dc5331efa9e97637eae3741682bc14ae993f0b7672ccc63c37f1f9

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

diff --git a/ext/session/sessionchange2.test b/ext/session/sessionchange2.test
new file mode 100644 (file)
index 0000000..4a211d8
--- /dev/null
@@ -0,0 +1,205 @@
+# 2026 January 09
+#
+# 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 [file join [file dirname [info script]] session_common.tcl]
+source $testdir/tester.tcl
+ifcapable !session {finish_test; return}
+
+set testprefix sessionchange2
+
+do_test 1.0 {
+  sqlite3changegroup grp
+  list [catch { grp change_begin INSERT "nosuchtable" 0 } msg] $msg
+} {1 {SQLITE_ERROR - no such table: nosuchtable}}
+
+do_test 1.1 {
+  grp schema db main
+  list [catch { grp change_begin INSERT "nosuchtable" 0 } msg] $msg
+} {1 {SQLITE_ERROR - no such table: nosuchtable}}
+
+do_execsql_test 1.2.1 {
+  CREATE TABLE t1(a INTEGER PRIMARY KEY, b);
+}
+do_test 1.2.2 {
+  list [catch { grp change_begin 435 "t1" 0 } msg] $msg
+} {1 SQLITE_ERROR}
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 2.0 {
+  CREATE TABLE t1(a INTEGER PRIMARY KEY, b);
+  CREATE TABLE t2(a, b);
+}
+
+do_test 2.1.1 {
+  sqlite3changegroup grp
+  grp schema db main
+  grp change_begin INSERT "t1" 0
+  grp change_int64 new 0 55
+  grp change_int64 new 1 101
+  grp change_finish false
+} {}
+
+do_test 2.1.2 {
+  set C [grp output]
+  grp delete
+  changeset_to_list $C
+} {
+  {INSERT t1 0 X. {} {i 55 i 101}}
+}
+
+do_test 2.2.1 {
+  sqlite3changegroup grp
+  grp schema db main
+  grp change_begin INSERT "t2" 0
+  grp change_int64 new 0 -5
+  grp change_int64 new 1 -10
+  grp change_int64 new 2 -20
+  grp change_finish false
+} {}
+
+do_test 2.2.2 {
+  set C [grp output]
+  grp delete
+  changeset_to_list $C
+} {
+  {INSERT t2 0 X.. {} {i -5 i -10 i -20}}
+}
+
+do_test 2.2.3 {
+  sqlite3changegroup grp
+  grp schema db main
+  grp change_begin INSERT "t1" 0
+  grp change_int64 new 0 223344
+  grp change_null  new 1
+  grp change_finish false
+} {}
+
+do_test 2.2.4 {
+  set C [grp output]
+  grp delete
+  changeset_to_list $C
+} {
+  {INSERT t1 0 X. {} {i 223344 n {}}}
+}
+
+do_test 2.2.5 {
+  sqlite3changegroup grp
+  grp schema db main
+  grp change_begin DELETE "t1" 0
+
+  grp change_int64 old 0 1
+  grp change_int64 old 1 123
+
+  grp change_finish false
+} {}
+
+do_test 2.2.6 {
+  set C [grp output]
+  grp delete
+  changeset_to_list $C
+} {
+  {DELETE t1 0 X. {i 1 i 123} {}}
+}
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 3.0 {
+  CREATE TABLE t1(a PRIMARY KEY, b, c);
+  CREATE TABLE t2(x, y);
+}
+
+foreach {tn script error} {
+  1 {
+    grp change_begin UPDATE t1 0
+  } {SQLITE_ERROR - invalid change: undefined value in PK of old.* record}
+
+  2 {
+    grp change_begin UPDATE t1 0
+    grp change_int64 old 0 1234
+    grp change_int64 new 0 5678
+  } {SQLITE_ERROR - invalid change: defined value in PK of new.* record}
+
+  3 {
+    grp change_begin UPDATE t1 0
+    grp change_null old 0
+  } {SQLITE_ERROR - invalid change: null value in PK of old.* record}
+
+  4 {
+    grp change_begin UPDATE t1 0
+    grp change_int64 old 0 1234
+    grp change_int64 old 1 20
+  } {SQLITE_ERROR - invalid change: column 1 - old.* value is defined but new.* is undefined}
+
+  5 {
+    grp change_begin UPDATE t1 0
+    grp change_int64 old 0 1234
+    grp change_int64 new 1 20
+  } {SQLITE_ERROR - invalid change: column 1 - old.* value is undefined but new.* is defined}
+
+  6 {
+    grp change_begin INSERT t1 0
+    grp change_null  new 0
+    grp change_int64 new 1 20
+  } {SQLITE_ERROR - invalid change: null value in PK}
+
+  7 {
+    grp change_begin INSERT t1 0
+    grp change_int64 new 1 20
+  } {SQLITE_ERROR - invalid change: column 0 is undefined}
+
+  8 {
+    grp change_begin INSERT t1 0
+    grp change_int64 new 0 20
+  } {SQLITE_ERROR - invalid change: column 1 is undefined}
+
+  9 {
+    grp change_begin DELETE t1 0
+    grp change_int64 old 0 20
+  } {SQLITE_ERROR - invalid change: column 1 is undefined}
+
+  10 {
+    grp change_begin DELETE t1 0
+    grp change_int64 old 1 20
+  } {SQLITE_ERROR - invalid change: column 0 is undefined}
+
+} {
+  sqlite3changegroup grp
+  grp schema db main
+  eval $script
+  do_test 3.1.$tn {
+    list [catch { grp change_finish false } msg] $msg
+  } [list 1 $error]
+  grp delete
+}
+
+
+do_test 3.2.1 {
+  sqlite3changegroup grp
+  grp schema db main
+  grp change_begin DELETE t1 0
+  list [catch {grp change_int64 new 0 20} msg] $msg
+} {1 SQLITE_ERROR}
+do_test 3.2.2 {
+  grp change_finish true
+  grp change_begin INSERT t1 0
+  list [catch {grp change_int64 old 0 20} msg] $msg
+} {1 SQLITE_ERROR}
+
+grp delete
+
+finish_test
+
index 90fedc6db43dcc01563b98ba1a3aa7f6e43d8cf4..278810e12cdd9dff78981a205589fb316017641f 100644 (file)
@@ -377,6 +377,19 @@ static void sessionPutI64(u8 *aBuf, sqlite3_int64 i){
   aBuf[7] = (i>> 0) & 0xFF;
 }
 
+/*
+** Write a double value to the buffer aBuf[].
+*/
+static void sessionPutDouble(u8 *aBuf, double r){
+  /* TODO: SQLite does something special to deal with mixed-endian
+  ** floating point values (e.g. ARM7). This code probably should
+  ** too.  */
+  u64 i;
+  assert( sizeof(double)==8 && sizeof(u64)==8 );
+  memcpy(&i, &r, 8);
+  sessionPutI64(aBuf, i);
+}
+
 /*
 ** This function is used to serialize the contents of value pValue (see
 ** comment titled "RECORD FORMAT" above).
@@ -414,16 +427,13 @@ static int sessionSerializeValue(
           /* 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);
+            u64 i = (u64)sqlite3_value_int64(pValue);
+            sessionPutI64(&aBuf[1], i);
           }else{
-            double r;
-            assert( sizeof(double)==8 && sizeof(u64)==8 );
-            r = sqlite3_value_double(pValue);
-            memcpy(&i, &r, 8);
+            double r = sqlite3_value_double(pValue);
+            sessionPutDouble(&aBuf[1], r);
           }
-          sessionPutI64(&aBuf[1], i);
         }
         nByte = 9; 
         break;
@@ -1355,9 +1365,7 @@ static void sessionUpdateOneChange(
           
         case SQLITE_FLOAT: {
           double rVal = sqlite3_column_double(pDflt, iField);
-          i64 iVal = 0;
-          memcpy(&iVal, &rVal, sizeof(rVal));
-          sessionPutI64(&pNew->aRecord[pNew->nRecord], iVal); 
+          sessionPutDouble(&pNew->aRecord[pNew->nRecord], rVal); 
           pNew->nRecord += 8;
           break;
         }
@@ -2614,15 +2622,14 @@ static void sessionAppendCol(
     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);
+        sqlite3_int64 i = sqlite3_column_int64(pStmt, iCol);
+        sessionPutI64(aBuf, i);
       }else{
         double r = sqlite3_column_double(pStmt, iCol);
-        memcpy(&i, &r, 8);
+        sessionPutDouble(aBuf, r);
       }
-      sessionPutI64(aBuf, i);
       sessionAppendBlob(p, aBuf, 8, pRc);
     }
     if( eType==SQLITE_BLOB || eType==SQLITE_TEXT ){
@@ -5649,6 +5656,21 @@ int sqlite3changeset_apply_strm(
   );
 }
 
+/*
+** The parts of the sqlite3_changegroup structure used by the
+** sqlite3changegroup_change_xxx() APIs.
+*/
+typedef struct ChangeData ChangeData;
+struct ChangeData {
+  SessionTable *pTab;
+  int bIndirect;
+  int eOp;
+
+  int nBufAlloc;
+  SessionBuffer *aBuf;
+  SessionBuffer record;
+};
+
 /*
 ** sqlite3_changegroup handle.
 */
@@ -5660,6 +5682,7 @@ struct sqlite3_changegroup {
 
   sqlite3 *db;                    /* Configured by changegroup_schema() */
   char *zDb;                      /* Configured by changegroup_schema() */
+  ChangeData cd;                  /* Used by changegroup_change_xxx() APIs. */
 };
 
 /*
@@ -5899,15 +5922,14 @@ static int sessionChangesetExtendRecord(
       switch( eType ){
         case SQLITE_FLOAT:
         case SQLITE_INTEGER: {
-          i64 iVal;
-          if( eType==SQLITE_INTEGER ){
-            iVal = sqlite3_column_int64(pTab->pDfltStmt, ii);
-          }else{
-            double rVal = sqlite3_column_int64(pTab->pDfltStmt, ii);
-            memcpy(&iVal, &rVal, sizeof(i64));
-          }
           if( SQLITE_OK==sessionBufferGrow(pOut, 8, &rc) ){
-            sessionPutI64(&pOut->aBuf[pOut->nBuf], iVal);
+            if( eType==SQLITE_INTEGER ){
+              sqlite3_int64 iVal = sqlite3_column_int64(pTab->pDfltStmt, ii);
+              sessionPutI64(&pOut->aBuf[pOut->nBuf], iVal);
+            }else{
+              double rVal = sqlite3_column_double(pTab->pDfltStmt, ii);
+              sessionPutDouble(&pOut->aBuf[pOut->nBuf], rVal);
+            }
             pOut->nBuf += 8;
           }
           break;
@@ -5978,13 +6000,19 @@ static int sessionChangesetFindTable(
   int nCol = 0;
 
   *ppTab = 0;
-  sqlite3changeset_pk(pIter, &abPK, &nCol);
 
   /* Search the list for an existing table */
   for(pTab = pGrp->pList; pTab; pTab=pTab->pNext){
     if( 0==sqlite3_strnicmp(pTab->zName, zTab, nTab+1) ) break;
   }
 
+
+  if( pIter ){
+    sqlite3changeset_pk(pIter, &abPK, &nCol);
+  }else if( !pTab && !pGrp->db ){
+    return SQLITE_OK;
+  }
+
   /* If one was not found above, create a new table now */
   if( !pTab ){
     SessionTable **ppNew;
@@ -6003,8 +6031,8 @@ static int sessionChangesetFindTable(
     if( pGrp->db ){
       pTab->nCol = 0;
       rc = sessionInitTable(0, pTab, pGrp->db, pGrp->zDb);
-      if( rc ){
-        assert( pTab->azCol==0 );
+      if( rc || pTab->nCol==0 ){
+        sqlite3_free(pTab->azCol);
         sqlite3_free(pTab);
         return rc;
       }
@@ -6019,7 +6047,7 @@ static int sessionChangesetFindTable(
   }
 
   /* Check that the table is compatible. */
-  if( !sessionChangesetCheckCompat(pTab, nCol, abPK) ){
+  if( pIter && !sessionChangesetCheckCompat(pTab, nCol, abPK) ){
     rc = SQLITE_SCHEMA;
   }
 
@@ -6028,44 +6056,27 @@ static int sessionChangesetFindTable(
 }
 
 /*
-** Add the change currently indicated by iterator pIter to the hash table
-** belonging to changegroup pGrp.
+** Add a single change to the changegroup pGrp.
 */
 static int sessionOneChangeToHash(
-  sqlite3_changegroup *pGrp,
-  sqlite3_changeset_iter *pIter,
-  int bRebase
+  sqlite3_changegroup *pGrp,      /* Changegroup to update */
+  SessionTable *pTab,             /* Table change pertains to */
+  int op,                         /* One of SQLITE_INSERT, UPDATE, DELETE */
+  int bIndirect,                  /* True to flag change as "indirect" */
+  int nCol,                       /* Number of columns in record(s) */
+  u8 *aRec,                       /* Serialized change record(s) */ 
+  int nRec,                       /* Size of aRec[] in bytes */
+  int bRebase                     /* True if this is a rebase blob */
 ){
   int rc = SQLITE_OK;
-  int nCol = 0;
-  int op = 0;
   int iHash = 0;
-  int bIndirect = 0;
   SessionChange *pChange = 0;
   SessionChange *pExist = 0;
   SessionChange **pp = 0;
-  SessionTable *pTab = 0;
-  u8 *aRec = &pIter->in.aData[pIter->in.iCurrent + 2];
-  int nRec = (pIter->in.iNext - pIter->in.iCurrent) - 2;
 
   assert( nRec>0 );
 
-  /* Ensure that only changesets, or only patchsets, but not a mixture
-  ** of both, are being combined. It is an error to try to combine a
-  ** changeset and a patchset.  */
-  if( pGrp->pList==0 ){
-    pGrp->bPatch = pIter->bPatchset;
-  }else if( pIter->bPatchset!=pGrp->bPatch ){
-    rc = SQLITE_ERROR;
-  }
-
-  if( rc==SQLITE_OK ){
-    const char *zTab = 0;
-    sqlite3changeset_op(pIter, &zTab, &nCol, &op, &bIndirect);
-    rc = sessionChangesetFindTable(pGrp, zTab, pIter, &pTab);
-  }
-
-  if( rc==SQLITE_OK && nCol<pTab->nCol ){
+  if( nCol<pTab->nCol ){
     SessionBuffer *pBuf = &pGrp->rec;
     rc = sessionChangesetExtendRecord(pGrp, pTab, nCol, op, aRec, nRec, pBuf);
     aRec = pBuf->aBuf;
@@ -6073,7 +6084,7 @@ static int sessionOneChangeToHash(
     assert( pGrp->db );
   }
 
-  if( rc==SQLITE_OK && sessionGrowHash(0, pIter->bPatchset, pTab) ){
+  if( rc==SQLITE_OK && sessionGrowHash(0, pGrp->bPatch, pTab) ){
     rc = SQLITE_NOMEM;
   }
 
@@ -6081,12 +6092,12 @@ static int sessionOneChangeToHash(
     /* Search for existing entry. If found, remove it from the hash table. 
     ** Code below may link it back in.  */
     iHash = sessionChangeHash(
-        pTab, (pIter->bPatchset && op==SQLITE_DELETE), aRec, pTab->nChange
+        pTab, (pGrp->bPatch && op==SQLITE_DELETE), aRec, pTab->nChange
     );
     for(pp=&pTab->apChange[iHash]; *pp; pp=&(*pp)->pNext){
       int bPkOnly1 = 0;
       int bPkOnly2 = 0;
-      if( pIter->bPatchset ){
+      if( pGrp->bPatch ){
         bPkOnly1 = (*pp)->op==SQLITE_DELETE;
         bPkOnly2 = op==SQLITE_DELETE;
       }
@@ -6101,7 +6112,7 @@ static int sessionOneChangeToHash(
 
   if( rc==SQLITE_OK ){
     rc = sessionChangeMerge(pTab, bRebase, 
-        pIter->bPatchset, pExist, op, bIndirect, aRec, nRec, &pChange
+        pGrp->bPatch, pExist, op, bIndirect, aRec, nRec, &pChange
     );
   }
   if( rc==SQLITE_OK && pChange ){
@@ -6110,6 +6121,47 @@ static int sessionOneChangeToHash(
     pTab->nEntry++;
   }
 
+  return rc;
+}
+
+/*
+** Add the change currently indicated by iterator pIter to the hash table
+** belonging to changegroup pGrp.
+*/
+static int sessionOneChangeIterToHash(
+  sqlite3_changegroup *pGrp,
+  sqlite3_changeset_iter *pIter,
+  int bRebase
+){
+  u8 *aRec = &pIter->in.aData[pIter->in.iCurrent + 2];
+  int nRec = (pIter->in.iNext - pIter->in.iCurrent) - 2;
+  const char *zTab = 0;
+  int nCol = 0;
+  int op = 0;
+  int bIndirect = 0;
+  int rc = SQLITE_OK;
+  SessionTable *pTab = 0;
+
+  /* Ensure that only changesets, or only patchsets, but not a mixture
+  ** of both, are being combined. It is an error to try to combine a
+  ** changeset and a patchset.  */
+  if( pGrp->pList==0 ){
+    pGrp->bPatch = pIter->bPatchset;
+  }else if( pIter->bPatchset!=pGrp->bPatch ){
+    rc = SQLITE_ERROR;
+  }
+
+  if( rc==SQLITE_OK ){
+    sqlite3changeset_op(pIter, &zTab, &nCol, &op, &bIndirect);
+    rc = sessionChangesetFindTable(pGrp, zTab, pIter, &pTab);
+  }
+
+  if( rc==SQLITE_OK ){
+    rc = sessionOneChangeToHash(
+        pGrp, pTab, op, bIndirect, nCol, aRec, nRec, bRebase
+    );
+  }
+
   if( rc==SQLITE_OK ) rc = pIter->rc;
   return rc;
 }
@@ -6129,7 +6181,7 @@ static int sessionChangesetToHash(
 
   pIter->in.bNoDiscard = 1;
   while( SQLITE_ROW==(sessionChangesetNext(pIter, &aRec, &nRec, 0)) ){
-    rc = sessionOneChangeToHash(pGrp, pIter, bRebase);
+    rc = sessionOneChangeIterToHash(pGrp, pIter, bRebase);
     if( rc!=SQLITE_OK ) break;
   }
 
@@ -6277,7 +6329,7 @@ int sqlite3changegroup_add_change(
     rc = SQLITE_ERROR;
   }else{
     pIter->in.bNoDiscard = 1;
-    rc = sessionOneChangeToHash(pGrp, pIter, 0);
+    rc = sessionOneChangeIterToHash(pGrp, pIter, 0);
   }
   return rc;
 }
@@ -6329,6 +6381,12 @@ int sqlite3changegroup_output_strm(
 */
 void sqlite3changegroup_delete(sqlite3_changegroup *pGrp){
   if( pGrp ){
+    int ii;
+    for(ii=0; ii<pGrp->cd.nBufAlloc; ii++){
+      sqlite3_free(pGrp->cd.aBuf[ii].aBuf);
+    }
+    sqlite3_free(pGrp->cd.record.aBuf);
+    sqlite3_free(pGrp->cd.aBuf);
     sqlite3_free(pGrp->zDb);
     sessionDeleteTable(0, pGrp->pList);
     sqlite3_free(pGrp->rec.aBuf);
@@ -6759,4 +6817,310 @@ int sqlite3session_config(int op, void *pArg){
   return rc;
 }
 
+/*
+** Begin adding a change to a changegroup object.
+*/
+int sqlite3changegroup_change_begin(
+  sqlite3_changegroup *pGrp,
+  int eOp,
+  const char *zTab,
+  int bIndirect,
+  char **pzErr
+){
+  SessionTable *pTab = 0;
+  int rc = SQLITE_OK;
+
+  if( pGrp->cd.pTab ){
+    rc = SQLITE_MISUSE;
+  }else if( eOp!=SQLITE_INSERT && eOp!=SQLITE_UPDATE && eOp!=SQLITE_DELETE ){
+    rc = SQLITE_ERROR;
+  }else{
+    rc = sessionChangesetFindTable(pGrp, zTab, 0, &pTab);
+  }
+  if( rc==SQLITE_OK ){
+    if( pTab==0 ){
+      if( pzErr ){
+        *pzErr = sqlite3_mprintf("no such table: %s", zTab);
+      }
+      rc = SQLITE_ERROR;
+    }else{
+      int nReq = pTab->nCol * (eOp==SQLITE_UPDATE ? 2 : 1);
+      pGrp->cd.pTab = pTab;
+      pGrp->cd.eOp = eOp;
+      pGrp->cd.bIndirect = bIndirect;
+
+      if( pGrp->cd.nBufAlloc<nReq ){
+        SessionBuffer *aBuf = (SessionBuffer*)sqlite3_realloc(
+            pGrp->cd.aBuf, nReq * sizeof(SessionBuffer)
+        );
+        if( aBuf==0 ){
+          rc = SQLITE_NOMEM;
+        }else{
+          memset(&aBuf[pGrp->cd.nBufAlloc], 0, 
+              sizeof(SessionBuffer) * (nReq - pGrp->cd.nBufAlloc)
+          );
+          pGrp->cd.aBuf = aBuf;
+          pGrp->cd.nBufAlloc = nReq;
+        }
+      }
+
+#ifdef SQLITE_DEBUG
+      {
+        /* Assert that all column values are currently undefined */
+        int ii;
+        for(ii=0; ii<pGrp->cd.nBufAlloc; ii++){
+          assert( pGrp->cd.aBuf[ii].nBuf==0 );
+        }
+      }
+#endif
+    }
+  }
+
+  return rc;
+}
+
+static int checkChangeParams(
+  sqlite3_changegroup *pGrp, 
+  int bNew,
+  int iCol,
+  sqlite3_int64 nReq,
+  SessionBuffer **ppBuf
+){
+  int rc = SQLITE_OK;
+  if( pGrp->cd.pTab==0 ){
+    rc = SQLITE_MISUSE;
+  }else if( iCol<0 || iCol>=pGrp->cd.pTab->nCol ){
+    rc = SQLITE_RANGE;
+  }else if( 
+      (bNew && pGrp->cd.eOp==SQLITE_DELETE)
+   || (!bNew && pGrp->cd.eOp==SQLITE_INSERT) 
+  ){
+    rc = SQLITE_ERROR;
+  }else{
+    SessionBuffer *pBuf = &pGrp->cd.aBuf[iCol]; 
+    if( pGrp->cd.eOp==SQLITE_UPDATE && bNew ){
+      pBuf += pGrp->cd.pTab->nCol;
+    }
+    pBuf->nBuf = 0;
+    sessionBufferGrow(pBuf, nReq, &rc);
+    pBuf->nBuf = nReq;
+    *ppBuf = pBuf;
+  }
+  return rc;
+}
+
+/*
+** Configure the change currently under construction with an integer value.
+*/
+int sqlite3changegroup_change_int64(
+  sqlite3_changegroup *pGrp, 
+  int bNew,
+  int iCol,
+  sqlite3_int64 iVal
+){
+  int rc = SQLITE_OK;
+  SessionBuffer *pBuf = 0;
+
+  if( SQLITE_OK!=(rc = checkChangeParams(pGrp, bNew, iCol, 9, &pBuf)) ){
+    return rc;
+  }
+
+  pBuf->aBuf[0] = SQLITE_INTEGER;
+  sessionPutI64(&pBuf->aBuf[1], iVal);
+  return SQLITE_OK;
+}
+
+/*
+** Configure the change currently under construction with a null value.
+*/
+int sqlite3changegroup_change_null(
+  sqlite3_changegroup *pGrp, 
+  int bNew,
+  int iCol
+){
+  int rc = SQLITE_OK;
+  SessionBuffer *pBuf = 0;
+
+  if( SQLITE_OK!=(rc = checkChangeParams(pGrp, bNew, iCol, 1, &pBuf)) ){
+    return rc;
+  }
+
+  pBuf->aBuf[0] = SQLITE_NULL;
+  return SQLITE_OK;
+}
+
+/*
+** Configure the change currently under construction with a real value.
+*/
+int sqlite3changegroup_change_double(
+  sqlite3_changegroup *pGrp, 
+  int bNew,
+  int iCol,
+  double fVal
+){
+  int rc = SQLITE_OK;
+  SessionBuffer *pBuf = 0;
+
+  if( SQLITE_OK!=(rc = checkChangeParams(pGrp, bNew, iCol, 9, &pBuf)) ){
+    return rc;
+  }
+
+  pBuf->aBuf[0] = SQLITE_FLOAT;
+  sessionPutDouble(&pBuf->aBuf[1], fVal);
+  return SQLITE_OK;
+}
+
+/*
+** Configure the change currently under construction with a text value.
+*/
+int sqlite3changegroup_change_text(
+  sqlite3_changegroup *pGrp, 
+  int bNew,
+  int iCol,
+  const char *pVal, 
+  int nVal
+){
+  int nText = nVal>=0 ? nVal : strlen(pVal);
+  sqlite3_int64 nByte = 1 + sessionVarintLen(nText) + nText;
+  int rc = SQLITE_OK;
+  SessionBuffer *pBuf = 0;
+
+  if( SQLITE_OK!=(rc = checkChangeParams(pGrp, bNew, iCol, nByte, &pBuf)) ){
+    return rc;
+  }
+
+  pBuf->aBuf[0] = SQLITE_TEXT;
+  pBuf->nBuf = (1 + sessionVarintPut(&pBuf->aBuf[1], nText));
+  memcpy(&pBuf->aBuf[pBuf->nBuf], pVal, nText);
+
+  return SQLITE_OK;
+}
+
+/*
+** Configure the change currently under construction with a text value.
+*/
+int sqlite3changegroup_change_blob(
+  sqlite3_changegroup *pGrp, 
+  int bNew,
+  int iCol,
+  const void *pVal, 
+  int nVal
+){
+  sqlite3_int64 nByte = 1 + sessionVarintLen(nVal) + nVal;
+  int rc = SQLITE_OK;
+  SessionBuffer *pBuf = 0;
+
+  if( SQLITE_OK!=(rc = checkChangeParams(pGrp, bNew, iCol, nByte, &pBuf)) ){
+    return rc;
+  }
+
+  pBuf->aBuf[0] = SQLITE_BLOB;
+  pBuf->nBuf = (1 + sessionVarintPut(&pBuf->aBuf[1], nVal));
+  memcpy(&pBuf->aBuf[pBuf->nBuf], pVal, nVal);
+
+  return SQLITE_OK;
+}
+
+int sqlite3changegroup_change_finish(
+  sqlite3_changegroup *pGrp, 
+  int bDiscard, 
+  char **pzErr
+){
+  int rc = SQLITE_OK;
+  if( pGrp->cd.pTab ){
+    SessionBuffer *aBuf = pGrp->cd.aBuf;
+    int ii;
+
+    if( bDiscard==0 ){
+      int nBuf = pGrp->cd.pTab->nCol;
+      int nRec = 0;
+      u8 eUndef = SQLITE_NULL;
+      if( pGrp->cd.eOp==SQLITE_UPDATE ){
+        for(ii=0; ii<nBuf; ii++){
+          if( pGrp->cd.pTab->abPK[ii] ){
+            if( aBuf[ii].nBuf<=1 ){
+              *pzErr = sqlite3_mprintf(
+                  "invalid change: %s value in PK of old.* record",
+                  aBuf[ii].nBuf==1 ? "null" : "undefined"
+              );
+              rc = SQLITE_ERROR;
+              break;
+            }else if( aBuf[ii + nBuf].nBuf>0 ){
+              *pzErr = sqlite3_mprintf(
+                  "invalid change: defined value in PK of new.* record"
+              );
+              rc = SQLITE_ERROR;
+              break;
+            }
+          }else if( (aBuf[ii].nBuf>0)!=(aBuf[ii+nBuf].nBuf>0) ){
+            *pzErr = sqlite3_mprintf(
+                "invalid change: column %d "
+                "- old.* value is %sdefined but new.* is %sdefined",
+                ii, aBuf[ii].nBuf ? "" : "un", aBuf[ii+nBuf].nBuf ? "" : "un"
+            );
+            rc = SQLITE_ERROR;
+            break;
+          }
+        }
+        eUndef = 0x00;
+        nBuf = nBuf * 2;
+      }else{
+        for(ii=0; ii<nBuf; ii++){
+          if( aBuf[ii].nBuf==0 ){
+            *pzErr = sqlite3_mprintf(
+                "invalid change: column %d is undefined", ii
+            );
+            rc = SQLITE_ERROR;
+            break;
+          }
+          if( aBuf[ii].nBuf==1 && pGrp->cd.pTab->abPK[ii] ){
+            *pzErr = sqlite3_mprintf(
+                "invalid change: null value in PK"
+            );
+            rc = SQLITE_ERROR;
+            break;
+          }
+        }
+      }
+
+      for(ii=0; ii<nBuf; ii++){
+        SessionBuffer *pBuf = &pGrp->cd.aBuf[ii];
+        nRec += (pBuf->nBuf ? pBuf->nBuf : 1);
+      }
+      if( 0==sessionBufferGrow(&pGrp->cd.record, nRec, &rc) ){
+        for(ii=0; ii<nBuf; ii++){
+          SessionBuffer *p = &pGrp->cd.aBuf[ii];
+          if( p->nBuf ){
+            memcpy(&pGrp->cd.record.aBuf[pGrp->cd.record.nBuf],p->aBuf,p->nBuf);
+            pGrp->cd.record.nBuf += p->nBuf;
+          }else{
+            pGrp->cd.record.aBuf[pGrp->cd.record.nBuf++] = eUndef;
+          }
+        }
+        rc = sessionOneChangeToHash(
+            pGrp,
+            pGrp->cd.pTab,
+            pGrp->cd.eOp,
+            pGrp->cd.bIndirect,
+            pGrp->cd.pTab->nCol,
+            pGrp->cd.record.aBuf,
+            pGrp->cd.record.nBuf,
+            0
+        );
+      }
+    }
+
+    {
+      int nZero = pGrp->cd.pTab->nCol;
+      if( pGrp->cd.eOp==SQLITE_UPDATE ) nZero += nZero;
+      for(ii=0; ii<nZero; ii++){
+        pGrp->cd.aBuf[ii].nBuf = 0;
+      }
+    }
+    pGrp->cd.pTab = 0;
+  }
+
+  return rc;
+}
+
 #endif /* SQLITE_ENABLE_SESSION && SQLITE_ENABLE_PREUPDATE_HOOK */
index 28b90eb6b511f46be001236430e355e96e041314..1c3af0e0b7c843d8cec86d2978860e8cc95a7ec7 100644 (file)
@@ -1853,6 +1853,171 @@ int sqlite3session_config(int op, void *pArg);
 */
 #define SQLITE_SESSION_CONFIG_STRMSIZE 1
 
+/*
+** CAPI3REF: Begin adding a change to a changegroup
+**
+** This API is used, in concert with other sqlite3changegroup_change_xxx()
+** APIs, to add changes to a changegroup object one at a time. To add a 
+** single change, the caller must:
+**
+**   1. Invoke sqlite3changegroup_change_begin() to indicate the type of
+**      change (INSERT, UPDATE or DELETE), the affected table and whether
+**      or not the change should be marked as indirect.
+**
+**   2. Invoke sqlite3changegroup_change_int64() or one of the other four
+**      value functions - _null(), _double(), _text() or _blob() - one or
+**      more times to specify old.* and new.* values for the change being
+**      constructed.
+**
+**   3. Invoke sqlite3changegroup_change_finish() to either finish adding
+**      the change to the group, or to discard the change altogether.
+**
+** The first argument to this function must be a pointer to the existing
+** changegroup object that the change will be added to. The second argument
+** must be SQLITE_INSERT, SQLITE_UPDATE or SQLITE_DELETE. The third is the
+** name of the table that the change affects, and the fourth is a boolean
+** flag specifying whether the change should be marked as "indirect" (if
+** bIndirect is non-zero) or not indirect (if bIndirect is zero).
+**
+** Following a successful call to this function, this function may not be
+** called again on the same changegroup until after
+** sqlite3changegroup_change_finish() has been called. Doing so is an
+** SQLITE_MISUSE error.
+**
+** The changegroup object passed as the first argument must be already 
+** configured with schema data for the specified table. It may be configured 
+** either by calling sqlite3changegroup_schema() with a database that contains
+** the table, or sqlite3changegroup_add() with a changeset that contains the 
+** table. If the changegroup object has not been configured with a schema for
+** the specified table when this function is called, SQLITE_ERROR is returned.
+**
+** If successful, SQLITE_OK is returned.
+*/
+int sqlite3changegroup_change_begin(
+  sqlite3_changegroup*,
+  int eOp,
+  const char *zTab,
+  int bIndirect,
+  char **pzErr
+);
+
+/*
+** This function may only be called between a succesful call to 
+** sqlite3changegroup_change_begin() and its matching
+** sqlite3changegroup_change_finish() call. If it is called at any
+** other time, it is an SQLITE_MISUSE error. Calling this function 
+** specifies a 64-bit integer value to be used in the change currently being
+** added to the changegroup object passed as the first argument.
+**
+** The second parameter, bNew, specifies whether the value is to be part of
+** the new.* (if bNew is non-zero) or old.* (if bNew is zero) record of
+** the change under construction. If this does not match the type of change
+** specified by the preceding call to sqlite3changegroup_change_begin() (i.e.
+** an old.* value for an SQLITE_INSERT change, or a new.* value for an
+** SQLITE_DELETE), then SQLITE_ERROR is returned.
+**
+** The third parameter specifies the column of the old.* or new.* record that
+** the value will be a part of. If the specified table has an explicit primary
+** key, then this is the index of the table column, numbered from 0 in the order
+** specified within the CREATE TABLE statement. Or, if the table uses an 
+** implicit rowid key, then the column 0 is the rowid and the explicit columns
+** are numbered starting from 1. If the iCol parameter is less than 0 or greater
+** than the index of the last column in the table, SQLITE_RANGE is returned.
+**
+** The fourth parameter is the integer value to use as part of the old.* or
+** new.* record.
+**
+** If this call is successful, SQLITE_OK is returned. Otherwise, an SQLite
+** error code.
+*/
+int sqlite3changegroup_change_int64(
+  sqlite3_changegroup*, 
+  int bNew,
+  int iCol,
+  sqlite3_int64 iVal
+);
+
+/*
+** This function is similar to sqlite3changegroup_change_int64(). Except that
+** it configures the change currently under construction with a NULL value
+** instead of a 64-bit integer.
+*/
+int sqlite3changegroup_change_null(sqlite3_changegroup*, int, int);
+
+/*
+** This function is similar to sqlite3changegroup_change_int64(). Except that
+** it configures the change currently being constructed with a real value
+** instead of a 64-bit integer.
+*/
+int sqlite3changegroup_change_double(sqlite3_changegroup*, int, int, double);
+
+/*
+** This function is similar to sqlite3changegroup_change_int64(). It configures
+** the currently accumulated change with a text value instead of a 64-bit
+** integer. Parameter pVal points to a buffer containing the text encoded using 
+** utf-8. Parameter nVal may either be the size of the text value in bytes, or
+** else a negative value, in which case the buffer pVal points to is assumed to
+** be nul-terminated.
+*/
+int sqlite3changegroup_change_text(
+  sqlite3_changegroup*, int, int, const char *pVal, int nVal
+);
+
+/*
+** This function is similar to sqlite3changegroup_change_int64(). It configures
+** the currently accumulated change with a blob value instead of a 64-bit
+** integer. Parameter pVal points to a buffer containing the blob. Parameter 
+** nVal is the size of the blob in bytes.
+*/
+int sqlite3changegroup_change_blob(
+    sqlite3_changegroup*, int, int, const void *pVal, int nVal
+);
+
+/*
+** This function may only be called following a successful call to
+** sqlite3changegroup_change_begin(). Otherwise, it is an SQLITE_MISUSE error.
+**
+** If parameter bDiscard is non-zero, then the current change is simply
+** discarded. In this case this function is always successful and SQLITE_OK
+** returned.
+**
+** If paramter bDiscard is zero, then an attempt is made to add the current
+** change to the changegroup. This requires that:
+**
+**   *  If the change is an INSERT or DELETE, then a value must be specified
+**      for all columns of the new.* or old.* record, respectively.
+**
+**   *  If the change is an UPDATE record, then values must be provided for
+**      the PRIMARY KEY columns of the old.* record, but must not be provided
+**      for PRIMARY KEY columns of the new.* record.
+**
+**   *  If the change is an UPDATE record, then for each non-PRIMARY KEY
+**      column in the old.* record for which a value has been provided, a
+**      value must also be provided for the same column in the new.* record.
+**      Similarly, for each non-PK column in the old.* record for which 
+**      a value is not provided, a value must not be provided for the same
+**      column in the new.* record.
+**
+**   *  All values specified for PRIMARY KEY columns must be non-NULL.
+**
+** Otherwise, it is an error. If the changegroup already contains a
+** change for the same row (identified by PRIMARY KEY columns), then the 
+** current change is combined with the existing change as for 
+** sqlite3changegroup_add().
+**
+** If the call is successful, SQLITE_OK is returned. Otherwise, if an error
+** occurs, an SQLite error code is returned. If an error is returned and
+** parameter pzErr is not NULL, then (*pzErr) may be set to point to a buffer
+** containing a nul-terminated utf-8 encoded English language error message.
+** It is the responsibility of the caller to free any such error message
+** buffer using sqlite3_free().
+*/
+int sqlite3changegroup_change_finish(
+  sqlite3_changegroup*, 
+  int bDiscard, 
+  char **pzErr
+);
+
 /*
 ** Make sure we can call this stuff from C++.
 */
index 6ad5b37749280c940d5c859be4186f63c54e66f0..d868db375807b899859ef0d3e361187a48928e57 100644 (file)
@@ -11,6 +11,8 @@
   typedef unsigned char u8;
 #endif
 
+extern const char *sqlite3ErrName(int);
+
 typedef struct TestSession TestSession;
 struct TestSession {
   sqlite3_session *pSession;
@@ -395,7 +397,6 @@ static int SQLITE_TCLAPI test_session_cmd(
       }
       rc = sqlite3session_object_config(pSession, aOpt[iOpt].opt, &iArg);
       if( rc!=SQLITE_OK ){
-        extern const char *sqlite3ErrName(int);
         Tcl_SetObjResult(interp, Tcl_NewStringObj(sqlite3ErrName(rc), -1));
       }else{
         Tcl_SetObjResult(interp, Tcl_NewIntObj(iArg));
@@ -1530,6 +1531,14 @@ static void test_changegroup_del(void *clientData){
   ckfree(pGrp);
 }
 
+static int testGetNewOrOld(Tcl_Interp *interp, Tcl_Obj *pObj, int *pbNew){
+  const char *azVal[] = { "old", "new", 0 };
+  int iIdx = 0;
+  int rc = Tcl_GetIndexFromObj(interp, pObj, azVal, "record", 0, &iIdx);
+  *pbNew = iIdx;
+  return rc;
+}
+
 /*
 ** Tclcmd:  $changegroup schema DB DBNAME
 ** Tclcmd:  $changegroup add CHANGESET
@@ -1547,14 +1556,21 @@ static int SQLITE_TCLAPI test_changegroup_cmd(
     const char *zSub;
     int nArg;
     const char *zMsg;
-    int iSub;
   } aSub[] = {
-    { "schema",       2, "DB DBNAME",  }, /* 0 */
-    { "add",          1, "CHANGESET",  }, /* 1 */
-    { "output",       0, "",           }, /* 2 */
-    { "delete",       0, "",           }, /* 3 */
-    { "add_change",   1, "ITERATOR",   }, /* 4 */
-    { 0 }
+    { "schema",        2, "DB DBNAME"            },    /* 0 */
+    { "add",           1, "CHANGESET"            },    /* 1 */
+    { "output",        0, ""                     },    /* 2 */
+    { "delete",        0, ""                     },    /* 3 */
+    { "add_change",    1, "ITERATOR"             },    /* 4 */
+
+    { "change_begin",  3, "TYPE TABLE INDIRECT"  },    /* 5 */
+    { "change_int64",  3, "[new|old] ICOL VALUE" },    /* 6 */
+    { "change_null",   2, "[new|old] ICOL"       },    /* 7 */
+    { "change_double", 3, "[new|old] ICOL VALUE" },    /* 8 */
+    { "change_text",   3, "[new|old] ICOL VALUE" },    /* 9 */
+    { "change_blob",   3, "[new|old] ICOL VALUE" },    /* 10 */
+    { "change_finish", 1, "BDISCARD"             },    /* 11 */
+    { 0, 0, 0 }
   };
   int rc = TCL_OK;
   int iSub = 0;
@@ -1623,6 +1639,145 @@ static int SQLITE_TCLAPI test_changegroup_cmd(
       break;
     };
 
+    case 5: {      /* change_begin */
+      struct ChangeType {
+        const char *zType;
+        int eType;
+      } aType[] = {
+        { "INSERT", SQLITE_INSERT },
+        { "UPDATE", SQLITE_UPDATE },
+        { "DELETE", SQLITE_DELETE },
+        { 0, 0 }
+      };
+      int eType = 0;
+      const char *zTab = 0;
+      int bIndirect;
+      int iIdx = 0;
+      char *zErr = 0;
+
+      if( TCL_OK!=Tcl_GetIntFromObj(0, objv[2], &eType) ){
+        rc = Tcl_GetIndexFromObjStruct(
+            interp, objv[2], aType, sizeof(aType[0]), "TYPE", 0, &iIdx
+        );
+        if( rc!=TCL_OK ) return rc;
+        eType = aType[iIdx].eType;
+      }
+      zTab = Tcl_GetString(objv[3]);
+      if( Tcl_GetBooleanFromObj(interp, objv[4], &bIndirect) ){
+        return TCL_ERROR;
+      }
+
+      rc = sqlite3changegroup_change_begin(p->pGrp, eType,zTab,bIndirect,&zErr);
+      assert( zErr==0 || rc!=SQLITE_OK );
+      if( rc!=SQLITE_OK ){
+        rc = test_session_error(interp, rc, zErr);
+      }
+
+      break;
+    }
+
+    case 6: {      /* change_int64 */
+      int bNew = 0;
+      int iCol = 0;
+      sqlite3_int64 iVal = 0;
+      if( TCL_OK!=testGetNewOrOld(interp, objv[2], &bNew)
+       || TCL_OK!=Tcl_GetIntFromObj(interp, objv[3], &iCol)
+       || TCL_OK!=Tcl_GetWideIntFromObj(interp, objv[4], &iVal)
+      ){
+        rc = TCL_ERROR;
+      }else{
+        rc = sqlite3changegroup_change_int64(p->pGrp, bNew, iCol, iVal);
+        if( rc!=SQLITE_OK ){
+          rc = test_session_error(interp, rc, 0);
+        }
+      }
+      break;
+    }
+
+    case 7: {      /* change_null */
+      int bNew = 0;
+      int iCol = 0;
+      if( TCL_OK!=testGetNewOrOld(interp, objv[2], &bNew)
+       || TCL_OK!=Tcl_GetIntFromObj(interp, objv[3], &iCol)
+      ){
+        rc = TCL_ERROR;
+      }else{
+        rc = sqlite3changegroup_change_null(p->pGrp, bNew, iCol);
+        if( rc!=SQLITE_OK ){
+          rc = test_session_error(interp, rc, 0);
+        }
+      }
+      break;
+    }
+
+    case 8: {      /* change_double */
+      int bNew = 0;
+      int iCol = 0;
+      double rVal = 0;
+      if( TCL_OK!=testGetNewOrOld(interp, objv[2], &bNew)
+       || TCL_OK!=Tcl_GetIntFromObj(interp, objv[3], &iCol)
+       || TCL_OK!=Tcl_GetDoubleFromObj(interp, objv[4], &rVal)
+      ){
+        rc = TCL_ERROR;
+      }else{
+        rc = sqlite3changegroup_change_double(p->pGrp, bNew, iCol, rVal);
+        if( rc!=SQLITE_OK ){
+          rc = test_session_error(interp, rc, 0);
+        }
+      }
+      break;
+    }
+
+    case 9: {      /* change_text */
+      int bNew = 0;
+      int iCol = 0;
+      if( TCL_OK!=testGetNewOrOld(interp, objv[2], &bNew)
+       || TCL_OK!=Tcl_GetIntFromObj(interp, objv[3], &iCol)
+      ){
+        rc = TCL_ERROR;
+      }else{
+        int nVal = 0;
+        const char *pVal = Tcl_GetStringFromObj(objv[4], &nVal);
+        rc = sqlite3changegroup_change_text(p->pGrp, bNew, iCol, pVal, nVal);
+        if( rc!=SQLITE_OK ){
+          rc = test_session_error(interp, rc, 0);
+        }
+      }
+      break;
+    }
+
+    case 10: {      /* change_blob */
+      int bNew = 0;
+      int iCol = 0;
+      if( TCL_OK!=testGetNewOrOld(interp, objv[2], &bNew)
+       || TCL_OK!=Tcl_GetIntFromObj(interp, objv[3], &iCol)
+      ){
+        rc = TCL_ERROR;
+      }else{
+        int nVal = 0;
+        const u8 *pVal = Tcl_GetByteArrayFromObj(objv[4], &nVal);
+        rc = sqlite3changegroup_change_blob(p->pGrp, bNew, iCol, pVal, nVal);
+        if( rc!=SQLITE_OK ){
+          rc = test_session_error(interp, rc, 0);
+        }
+      }
+      break;
+    }
+
+    case 11: {      /* change_finish */
+      int bDiscard = 0;
+      if( TCL_OK!=Tcl_GetBooleanFromObj(interp, objv[2], &bDiscard) ){
+        rc = TCL_ERROR;
+      }else{
+        char *zErr = 0;
+        rc = sqlite3changegroup_change_finish(p->pGrp, bDiscard, &zErr);
+        if( rc!=SQLITE_OK ){
+          rc = test_session_error(interp, rc, zErr);
+        }
+      }
+      break;
+    }
+
     default: {     /* delete */
       assert( iSub==3 );
       Tcl_DeleteCommand(interp, Tcl_GetString(objv[0]));
index 6319be53e682533580e686406c3c218e58539839..e8378621b923f7d8eb866c25e15ef983cf1196c5 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C wasm:\sfilter\sthe\scustom\sModule.instantiateWasm()\sout\sof\snode\sbuilds,\sper\srequest\sfrom\sthe\snpm\sproject.
-D 2026-01-12T15:43:18.126
+C Add\san\sAPI\sto\sthe\ssessions\smodule\sto\sadd\schanges\sone\sat\sa\stime\sto\san\ssqlite3_changegroup\sobject.
+D 2026-01-12T16:39:27.740
 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x
 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
@@ -562,6 +562,7 @@ F ext/session/sessionat.test 00c8badb35e43a2f12a716d2734a44d614ff62361979b6b8541
 F ext/session/sessionbig.test 47c381e7acfabeef17d98519a3080d69151723354d220afa2053852182ca7adf
 F ext/session/sessionblob.test 87faf667870b72f08e91969abd9f52a383ab7b514506ee194d64a39d8faff00a
 F ext/session/sessionchange.test 6618cb1c1338a4b6df173b6ac42d09623fb71269962abf23ebb7617fe9f45a50
+F ext/session/sessionchange2.test 9f8eef9a673e9cdc0c0d8eee0fe4f27295ae6e491a449542a44ba23e763c4258
 F ext/session/sessionconflict.test 19e4a53795c4c930bfec49e809311e09b2a9e202d9446e56d7a8b139046a0c07 x
 F ext/session/sessiondiff.test e89f7aedcdd89e5ebac3a455224eb553a171e9586fc3e1e6a7b3388d2648ba8d
 F ext/session/sessionfault.test c2b43d01213b389a3f518e90775fca2120812ba51e50444c4066962263e45c11
@@ -577,9 +578,9 @@ F ext/session/sessionrowid.test 85187c2f1b38861a5844868126f69f9ec62223a03449a98a
 F ext/session/sessionsize.test 8fcf4685993c3dbaa46a24183940ab9f5aa9ed0d23e5fb63bfffbdb56134b795
 F ext/session/sessionstat1.test 5e718d5888c0c49bbb33a7a4f816366db85f59f6a4f97544a806421b85dc2dec
 F ext/session/sessionwor.test 6fd9a2256442cebde5b2284936ae9e0d54bde692d0f5fd009ecef8511f4cf3fc
-F ext/session/sqlite3session.c b3de195ce668cace9b324599bf6255a70290cbfb5451e826e946f3aee6e64c54
-F ext/session/sqlite3session.h 7404723606074fcb2afdc6b72c206072cdb2b7d8ba097ca1559174a80bc26f7a
-F ext/session/test_session.c 8766b5973a6323934cb51248f621c3dc87ad2a98f023c3cc280d79e7d78d36fb
+F ext/session/sqlite3session.c f13ff9db3d1a492820fe9845739561a9336f066a606adf14ed679a7a6450bfa8
+F ext/session/sqlite3session.h d45d8dcc32bd0454b65fbb08d2a86f6d68d918f3c9dbf01dbe8540ee81954003
+F ext/session/test_session.c 86f5dae26de28f7091fc4913b4b8880ce764f24fb8d76eafba8c0a33125b2462
 F ext/wasm/GNUmakefile c3d007dd181527283d8674c812cc60518353f1f69c9a9d3008f10f53cea4a3c1
 F ext/wasm/README-dist.txt f01081a850ce38a56706af6b481e3a7878e24e42b314cfcd4b129f0f8427066a
 F ext/wasm/README.md 2e87804e12c98f1d194b7a06162a88441d33bb443efcfe00dc6565a780d2f259
@@ -2191,8 +2192,11 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee
 F tool/warnings.sh d924598cf2f55a4ecbc2aeb055c10bd5f48114793e7ba25f9585435da29e7e98
 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f
 F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c
-P 70b1da718c176b8eb154fe087af4352eb6f55c9c0d1f09fc625d073d9f8075f4
-R 00bfa5500890db22f6793537f5f0589c
-U stephan
-Z e94aded409cc10495252667056d88c93
+P b57a8215f4259a0aae188b7ee5060f8ff48919303179aae80b58b43ed3b991f5
+R 3bc6861aec8f49b745d80fe907b58d8c
+T *branch * changegroup-change-api
+T *sym-changegroup-change-api *
+T -sym-trunk *
+U dan
+Z a079c2735ef0be18051333989922e875
 # Remove this line to create a well-formed Fossil manifest.
index bec971799ff1b8ee641c166c7aeb22d12c785393..09cbfba6cbf6499453e90333386ffc52b5f55c84 100644 (file)
@@ -1,2 +1,2 @@
-branch trunk
-tag trunk
+branch changegroup-change-api
+tag changegroup-change-api
index d1c286cbbcb5ddf77e0b6457ae0b2894b85a7289..4f43bd95513bce5f9437aea697018e89e7cc0ae4 100644 (file)
@@ -1 +1 @@
-b57a8215f4259a0aae188b7ee5060f8ff48919303179aae80b58b43ed3b991f5
+27150a8c22dc5331efa9e97637eae3741682bc14ae993f0b7672ccc63c37f1f9