From 817c5093652b3e09da225d2ea30d9db1c39b9e8e Mon Sep 17 00:00:00 2001 From: dan Date: Mon, 12 Jan 2026 16:39:27 +0000 Subject: [PATCH 1/4] Add an API to the sessions module to add changes one at a time to an sqlite3_changegroup object. FossilOrigin-Name: 27150a8c22dc5331efa9e97637eae3741682bc14ae993f0b7672ccc63c37f1f9 --- ext/session/sessionchange2.test | 205 ++++++++++++++ ext/session/sqlite3session.c | 482 ++++++++++++++++++++++++++++---- ext/session/sqlite3session.h | 165 +++++++++++ ext/session/test_session.c | 171 ++++++++++- manifest | 22 +- manifest.tags | 4 +- manifest.uuid | 2 +- 7 files changed, 972 insertions(+), 79 deletions(-) create mode 100644 ext/session/sessionchange2.test diff --git a/ext/session/sessionchange2.test b/ext/session/sessionchange2.test new file mode 100644 index 0000000000..4a211d8c93 --- /dev/null +++ b/ext/session/sessionchange2.test @@ -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 + diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c index 90fedc6db4..278810e12c 100644 --- a/ext/session/sqlite3session.c +++ b/ext/session/sqlite3session.c @@ -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 && nColnCol ){ + if( nColnCol ){ 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; iicd.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.nBufAlloccd.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; iicd.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; iicd.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; iicd.pTab->abPK[ii] ){ + *pzErr = sqlite3_mprintf( + "invalid change: null value in PK" + ); + rc = SQLITE_ERROR; + break; + } + } + } + + for(ii=0; iicd.aBuf[ii]; + nRec += (pBuf->nBuf ? pBuf->nBuf : 1); + } + if( 0==sessionBufferGrow(&pGrp->cd.record, nRec, &rc) ){ + for(ii=0; iicd.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; iicd.aBuf[ii].nBuf = 0; + } + } + pGrp->cd.pTab = 0; + } + + return rc; +} + #endif /* SQLITE_ENABLE_SESSION && SQLITE_ENABLE_PREUPDATE_HOOK */ diff --git a/ext/session/sqlite3session.h b/ext/session/sqlite3session.h index 28b90eb6b5..1c3af0e0b7 100644 --- a/ext/session/sqlite3session.h +++ b/ext/session/sqlite3session.h @@ -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++. */ diff --git a/ext/session/test_session.c b/ext/session/test_session.c index 6ad5b37749..d868db3758 100644 --- a/ext/session/test_session.c +++ b/ext/session/test_session.c @@ -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])); diff --git a/manifest b/manifest index 6319be53e6..e8378621b9 100644 --- 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. diff --git a/manifest.tags b/manifest.tags index bec971799f..09cbfba6cb 100644 --- a/manifest.tags +++ b/manifest.tags @@ -1,2 +1,2 @@ -branch trunk -tag trunk +branch changegroup-change-api +tag changegroup-change-api diff --git a/manifest.uuid b/manifest.uuid index d1c286cbbc..4f43bd9551 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -b57a8215f4259a0aae188b7ee5060f8ff48919303179aae80b58b43ed3b991f5 +27150a8c22dc5331efa9e97637eae3741682bc14ae993f0b7672ccc63c37f1f9 From c289cf57a72cc317963a0d590c54cf02cd839aa6 Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 13 Jan 2026 19:59:47 +0000 Subject: [PATCH 2/4] Allow the APIs on this branch to be used to create patchsets as well as changesets. FossilOrigin-Name: 8699fe5b24c8aeb5e552aa7f4995f2b50dd33d1febd23ec1c07c8fc72497da72 --- ext/session/sessionchange2.test | 129 +++++++++++++++++++++++++++++++- ext/session/sqlite3session.c | 67 ++++++++++++++--- ext/session/sqlite3session.h | 46 +++++++++++- ext/session/test_session.c | 31 ++++++++ manifest | 21 +++--- manifest.uuid | 2 +- 6 files changed, 270 insertions(+), 26 deletions(-) diff --git a/ext/session/sessionchange2.test b/ext/session/sessionchange2.test index 4a211d8c93..6a508ef221 100644 --- a/ext/session/sessionchange2.test +++ b/ext/session/sessionchange2.test @@ -186,7 +186,6 @@ foreach {tn script error} { grp delete } - do_test 3.2.1 { sqlite3changegroup grp grp schema db main @@ -201,5 +200,133 @@ do_test 3.2.2 { grp delete +#------------------------------------------------------------------------- +reset_db +do_execsql_test 3.0 { + CREATE TABLE t1(a INTEGER PRIMARY KEY, b); + CREATE TABLE t2(a, b); +} + +proc do_changegroup_script_test {tn script changeset patchset} { + +breakpoint + sqlite3changegroup grp + grp schema db main + eval $script + do_test $tn.1 { + changeset_to_list [grp output] + } [list {*}$changeset] + grp delete + + sqlite3changegroup grp + grp config patchset 1 + grp schema db main + eval $script + do_test $tn.2 { + changeset_to_list [grp output] + } [list {*}$patchset] + grp delete + +} + +proc do_changegroup_patchset_test {tn script patchset} { + + sqlite3changegroup grp + grp config patchset 1 + grp schema db main + eval $script + do_test $tn { + changeset_to_list [grp output] + } [list {*}$patchset] + grp delete + +} + +do_changegroup_script_test 3.1 { + grp change_begin UPDATE t1 0 + grp change_int64 old 0 123 + grp change_int64 old 1 456 + grp change_int64 new 1 789 + grp change_finish false + + grp change_begin INSERT t1 0 + grp change_int64 new 0 124 + grp change_text new 1 hello_world + grp change_finish false + + grp change_begin DELETE t1 0 + grp change_int64 old 0 125 + grp change_double old 1 45.67 + grp change_finish false +} { + {DELETE t1 0 X. {i 125 f 45.67} {}} + {INSERT t1 0 X. {} {i 124 t hello_world}} + {UPDATE t1 0 X. {i 123 i 456} {{} {} i 789}} +} { + {DELETE t1 0 X. {i 125 {} {}} {}} + {INSERT t1 0 X. {} {i 124 t hello_world}} + {UPDATE t1 0 X. {i 123 {} {}} {{} {} i 789}} +} + +do_changegroup_script_test 3.1.2 { + grp change_begin INSERT t1 0 + grp change_int64 new 0 124 + grp change_text new 1 hello_world + grp change_finish false +} { + {INSERT t1 0 X. {} {i 124 t hello_world}} +} { + {INSERT t1 0 X. {} {i 124 t hello_world}} +} + +do_changegroup_patchset_test 3.2 { + grp change_begin UPDATE t1 0 + grp change_int64 old 0 123 + grp change_int64 new 1 789 + grp change_finish false + + grp change_begin DELETE t1 1 + grp change_int64 old 0 122 + grp change_finish false +} { + {DELETE t1 1 X. {i 122 {} {}} {}} + {UPDATE t1 0 X. {i 123 {} {}} {{} {} i 789}} +} + +do_changegroup_patchset_test 3.3 { + grp change_begin DELETE t1 0 + grp change_int64 old 0 123 + grp change_finish false +} { + {DELETE t1 0 X. {i 123 {} {}} {}} +} + +do_changegroup_script_test 3.4 { + grp change_begin INSERT t1 0 + grp change_int64 new 0 124 + grp change_text new 1 hello_world + grp change_finish false + + grp change_begin DELETE t1 0 + grp change_int64 old 0 124 + grp change_text old 1 hello_world + grp change_finish false + + grp change_begin INSERT t1 0 + grp change_int64 new 0 1000 + grp change_blob new 1 abcd + grp change_finish false + + grp change_begin UPDATE t1 0 + grp change_int64 old 0 1000 + grp change_blob old 1 abcd + grp change_blob new 1 bcda + grp change_finish false +} { + {INSERT t1 0 X. {} {i 1000 b bcda}} +} { + {INSERT t1 0 X. {} {i 1000 b bcda}} +} + finish_test diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c index 278810e12c..feabdf5c12 100644 --- a/ext/session/sqlite3session.c +++ b/ext/session/sqlite3session.c @@ -6271,6 +6271,33 @@ int sqlite3changegroup_new(sqlite3_changegroup **pp){ return rc; } +/* +** Configure a changegroup object. +*/ +int sqlite3changegroup_config( + sqlite3_changegroup *pGrp, + int op, + void *pArg +){ + int rc = SQLITE_OK; + + switch( op ){ + case SQLITE_CHANGEGROUP_CONFIG_PATCHSET: { + int arg = *(int*)pArg; + if( pGrp->pList==0 && arg>=0 ){ + pGrp->bPatch = (arg>0); + } + *(int*)pArg = pGrp->bPatch; + break; + } + default: + rc = SQLITE_MISUSE; + break; + } + + return rc; +} + /* ** Provide a database schema to the changegroup object. */ @@ -6879,6 +6906,10 @@ int sqlite3changegroup_change_begin( return rc; } +/* +** This function does processing common to the _change_int64(), _change_text() +** and other similar APIs. +*/ static int checkChangeParams( sqlite3_changegroup *pGrp, int bNew, @@ -6992,6 +7023,7 @@ int sqlite3changegroup_change_text( pBuf->aBuf[0] = SQLITE_TEXT; pBuf->nBuf = (1 + sessionVarintPut(&pBuf->aBuf[1], nText)); memcpy(&pBuf->aBuf[pBuf->nBuf], pVal, nText); + pBuf->nBuf += nText; return SQLITE_OK; } @@ -7017,6 +7049,7 @@ int sqlite3changegroup_change_blob( pBuf->aBuf[0] = SQLITE_BLOB; pBuf->nBuf = (1 + sessionVarintPut(&pBuf->aBuf[1], nVal)); memcpy(&pBuf->aBuf[pBuf->nBuf], pVal, nVal); + pBuf->nBuf += nVal; return SQLITE_OK; } @@ -7033,7 +7066,6 @@ int sqlite3changegroup_change_finish( 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; ii0)!=(aBuf[ii+nBuf].nBuf>0) ){ + }else + if( pGrp->bPatch==0 && (aBuf[ii].nBuf>0)!=(aBuf[ii+nBuf].nBuf>0) ){ *pzErr = sqlite3_mprintf( "invalid change: column %d " "- old.* value is %sdefined but new.* is %sdefined", @@ -7063,17 +7096,20 @@ int sqlite3changegroup_change_finish( } } eUndef = 0x00; - nBuf = nBuf * 2; + if( pGrp->bPatch==0 ) nBuf = nBuf * 2; }else{ for(ii=0; iicd.pTab->abPK[ii]; + if( (pGrp->cd.eOp==SQLITE_INSERT || pGrp->bPatch==0 || isPK) + && 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] ){ + if( aBuf[ii].nBuf==1 && isPK ){ *pzErr = sqlite3_mprintf( "invalid change: null value in PK" ); @@ -7083,13 +7119,19 @@ int sqlite3changegroup_change_finish( } } + pGrp->cd.record.nBuf = 0; for(ii=0; iicd.aBuf[ii]; - nRec += (pBuf->nBuf ? pBuf->nBuf : 1); - } - if( 0==sessionBufferGrow(&pGrp->cd.record, nRec, &rc) ){ - for(ii=0; iicd.aBuf[ii]; + SessionBuffer *p = &pGrp->cd.aBuf[ii]; + if( pGrp->bPatch ){ + if( pGrp->cd.pTab->abPK[ii]==0 ){ + if( pGrp->cd.eOp==SQLITE_UPDATE ){ + p += pGrp->cd.pTab->nCol; + }else if( pGrp->cd.eOp==SQLITE_DELETE ){ + continue; + } + } + } + if( 0==sessionBufferGrow(&pGrp->cd.record, p->nBuf?p->nBuf:1, &rc) ){ if( p->nBuf ){ memcpy(&pGrp->cd.record.aBuf[pGrp->cd.record.nBuf],p->aBuf,p->nBuf); pGrp->cd.record.nBuf += p->nBuf; @@ -7097,6 +7139,8 @@ int sqlite3changegroup_change_finish( pGrp->cd.record.aBuf[pGrp->cd.record.nBuf++] = eUndef; } } + } + if( rc==SQLITE_OK ){ rc = sessionOneChangeToHash( pGrp, pGrp->cd.pTab, @@ -7110,6 +7154,7 @@ int sqlite3changegroup_change_finish( } } + /* Reset all aBuf[] entries to "undefined". */ { int nZero = pGrp->cd.pTab->nCol; if( pGrp->cd.eOp==SQLITE_UPDATE ) nZero += nZero; diff --git a/ext/session/sqlite3session.h b/ext/session/sqlite3session.h index 1c3af0e0b7..36dfc504e9 100644 --- a/ext/session/sqlite3session.h +++ b/ext/session/sqlite3session.h @@ -1853,6 +1853,43 @@ int sqlite3session_config(int op, void *pArg); */ #define SQLITE_SESSION_CONFIG_STRMSIZE 1 +/* +** CAPI3REF: Configure a changegroup object +** +** Configure the changegroup object passes as the first argument. +** At present the only valid value for the second parameter is +** [SQLITE_CHANGEGROUP_CONFIG_PATCHSET]. +*/ +int sqlite3changegroup_config(sqlite3_changegroup*, int, void *pArg); + +/* +** CAPI3REF: Options for sqlite3changegroup_config(). +** +** The following values may passed as the the 2nd parameter to +** sqlite3session_object_config(). +** +**
SQLITE_CHANGEGROUP_CONFIG_PATCHSET
+** A changegroup object generates either a changeset or patchset. Usually, +** which is determined by whether the first call to sqlite3changegroup_add() +** is passed a changeset or a patchset. Or, if the first changes are added +** to the changegroup object using the sqlite3changegroup_change_xxx() +** APIs, then this option may be used to configure whether the changegroup +** object generates a changeset or patchset. +** +** When this option is invoked, parameter pArg must point to a value of +** type int. If the changegroup currently contains zero changes, and the +** value of the int variable is zero or greater than zero, then the +** changegroup is configured to generate a changeset or patchset, +** respectively. It is not an error if the changegroup is not configured +** because it has already started accumuting changes, just a no-op. +** +** Before returning, the int variable is set to 0 if the changegroup is +** configured to generate a changeset, or 1 if it is configured to generate +** a patchset. +*/ +#define SQLITE_CHANGEGROUP_CONFIG_PATCHSET 1 + + /* ** CAPI3REF: Begin adding a change to a changegroup ** @@ -1982,7 +2019,8 @@ int sqlite3changegroup_change_blob( ** returned. ** ** If paramter bDiscard is zero, then an attempt is made to add the current -** change to the changegroup. This requires that: +** change to the changegroup. Assuming the changegroup is configured to +** produce a changeset (not a patchset), 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. @@ -2005,6 +2043,12 @@ int sqlite3changegroup_change_blob( ** current change is combined with the existing change as for ** sqlite3changegroup_add(). ** +** For a patchset, all of the above rules apply except that it doesn't matter +** whether or not values are provided for the non-PK old.* record columns +** for an UPDATE or DELETE change. This means that code used to produce +** a changeset using the sqlite3changegroup_change_xxx() APIs may also +** be used to produce patchsets. +** ** 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 diff --git a/ext/session/test_session.c b/ext/session/test_session.c index d868db3758..03929284db 100644 --- a/ext/session/test_session.c +++ b/ext/session/test_session.c @@ -1570,6 +1570,8 @@ static int SQLITE_TCLAPI test_changegroup_cmd( { "change_text", 3, "[new|old] ICOL VALUE" }, /* 9 */ { "change_blob", 3, "[new|old] ICOL VALUE" }, /* 10 */ { "change_finish", 1, "BDISCARD" }, /* 11 */ + + { "config", 2, "OPTION INTVAL" }, /* 12 */ { 0, 0, 0 } }; int rc = TCL_OK; @@ -1778,6 +1780,35 @@ static int SQLITE_TCLAPI test_changegroup_cmd( break; } + case 12: { /* config */ + struct OptionName { + const char *zOpt; + int op; + } aOp[] = { + { "patchset", SQLITE_CHANGEGROUP_CONFIG_PATCHSET }, + { 0, 0 } + }; + int iIdx = 0; + int iArg = 0; + rc = Tcl_GetIndexFromObjStruct( + interp, objv[2], aOp, sizeof(aOp[0]), "option", 0, &iIdx + ); + if( rc==TCL_OK + && (rc = Tcl_GetIntFromObj(interp, objv[3], &iArg))==TCL_OK + ){ + int op = aOp[iIdx].op; + void *pArg = (void*)&iArg; + + rc = sqlite3changegroup_config(p->pGrp, op, pArg); + if( rc!=SQLITE_OK ){ + rc = test_session_error(interp, rc, 0); + }else{ + Tcl_SetObjResult(interp, Tcl_NewIntObj(iArg)); + } + } + break; + } + default: { /* delete */ assert( iSub==3 ); Tcl_DeleteCommand(interp, Tcl_GetString(objv[0])); diff --git a/manifest b/manifest index e8378621b9..c4852e1c4a 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -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 +C Allow\sthe\sAPIs\son\sthis\sbranch\sto\sbe\sused\sto\screate\spatchsets\sas\swell\sas\schangesets. +D 2026-01-13T19:59:47.818 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -562,7 +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/sessionchange2.test 601d8151b7faeef300355b9eaea65c26d823746e83ccaa8d674198a12d6641dd F ext/session/sessionconflict.test 19e4a53795c4c930bfec49e809311e09b2a9e202d9446e56d7a8b139046a0c07 x F ext/session/sessiondiff.test e89f7aedcdd89e5ebac3a455224eb553a171e9586fc3e1e6a7b3388d2648ba8d F ext/session/sessionfault.test c2b43d01213b389a3f518e90775fca2120812ba51e50444c4066962263e45c11 @@ -578,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 f13ff9db3d1a492820fe9845739561a9336f066a606adf14ed679a7a6450bfa8 -F ext/session/sqlite3session.h d45d8dcc32bd0454b65fbb08d2a86f6d68d918f3c9dbf01dbe8540ee81954003 -F ext/session/test_session.c 86f5dae26de28f7091fc4913b4b8880ce764f24fb8d76eafba8c0a33125b2462 +F ext/session/sqlite3session.c 16c36e63d41e2d0f6a8d4593618f221185064ab01d49b8514cb5e710642bdb67 +F ext/session/sqlite3session.h cb6d25b573751afd9668f9ae8f1c7819df30916f43cc4c06b57749a6c2ce3e10 +F ext/session/test_session.c c4164b818cbdfaa3ebbcde31a9a620a63478478154715276ce09bced8918429b F ext/wasm/GNUmakefile c3d007dd181527283d8674c812cc60518353f1f69c9a9d3008f10f53cea4a3c1 F ext/wasm/README-dist.txt f01081a850ce38a56706af6b481e3a7878e24e42b314cfcd4b129f0f8427066a F ext/wasm/README.md 2e87804e12c98f1d194b7a06162a88441d33bb443efcfe00dc6565a780d2f259 @@ -2192,11 +2192,8 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee F tool/warnings.sh d924598cf2f55a4ecbc2aeb055c10bd5f48114793e7ba25f9585435da29e7e98 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c -P b57a8215f4259a0aae188b7ee5060f8ff48919303179aae80b58b43ed3b991f5 -R 3bc6861aec8f49b745d80fe907b58d8c -T *branch * changegroup-change-api -T *sym-changegroup-change-api * -T -sym-trunk * +P 27150a8c22dc5331efa9e97637eae3741682bc14ae993f0b7672ccc63c37f1f9 +R f6f93392cfb2ac7fdf4dfeb41e3a044d U dan -Z a079c2735ef0be18051333989922e875 +Z b3689361f6f700b24a921b70fab2026e # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 4f43bd9551..4b3655ff91 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -27150a8c22dc5331efa9e97637eae3741682bc14ae993f0b7672ccc63c37f1f9 +8699fe5b24c8aeb5e552aa7f4995f2b50dd33d1febd23ec1c07c8fc72497da72 From 613cf76c2f3e20e46d0c841b64f19142ea6b1c57 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 14 Jan 2026 15:27:26 +0000 Subject: [PATCH 3/4] Add tests for edge cases in the new code on this branch. FossilOrigin-Name: cd115f0f4437bf254058e40d70ba4e82e87227d2752695aa18f8431c3abc5c0b --- ext/session/sessionchange2.test | 72 ++++++++++++++++++++++++++++++++- ext/session/sessionfault3.test | 16 +++++++- ext/session/test_session.c | 51 ++++++++++++++++------- manifest | 16 ++++---- manifest.uuid | 2 +- 5 files changed, 130 insertions(+), 27 deletions(-) diff --git a/ext/session/sessionchange2.test b/ext/session/sessionchange2.test index 6a508ef221..64208d223c 100644 --- a/ext/session/sessionchange2.test +++ b/ext/session/sessionchange2.test @@ -20,16 +20,20 @@ ifcapable !session {finish_test; return} set testprefix sessionchange2 -do_test 1.0 { +do_test 1.1.1 { sqlite3changegroup grp list [catch { grp change_begin INSERT "nosuchtable" 0 } msg] $msg } {1 {SQLITE_ERROR - no such table: nosuchtable}} -do_test 1.1 { +do_test 1.1.2 { grp schema db main list [catch { grp change_begin INSERT "nosuchtable" 0 } msg] $msg } {1 {SQLITE_ERROR - no such table: nosuchtable}} +do_test 1.1.3 { + list [catch { grp change_begin_ne INSERT "nosuchtable" 0 } msg] $msg +} {1 SQLITE_ERROR} + do_execsql_test 1.2.1 { CREATE TABLE t1(a INTEGER PRIMARY KEY, b); } @@ -328,5 +332,69 @@ do_changegroup_script_test 3.4 { {INSERT t1 0 X. {} {i 1000 b bcda}} } +do_changegroup_script_test 3.5 { + grp change_begin DELETE t1 0 + grp change_int64 old 0 1000 + grp change_text-1 old 1 "hello world" + grp change_finish false +} { + {DELETE t1 0 X. {i 1000 t {hello world}} {}} +} { + {DELETE t1 0 X. {i 1000 {} {}} {}} +} + +#------------------------------------------------------------------------- + +# Calling sqlite3changegroup_change_finish() if a change has not been +# started is a no-op. +# +do_test 4.0 { + sqlite3changegroup grp + grp change_finish false +} {} + +# But calling any other sqlite3changegroup_change_xxx() function if a +# change has not been started is a misuse. +# +foreach {tn cmd} { + 1 { grp change_blob old 0 abcd } + 2 { grp change_text new 1 helloworld } + 3 { grp change_int64 old 2 12345678 } + 4 { grp change_double new 3 12.345 } + 5 { grp change_null old 4 } +} { + do_test 4.1.$tn { + list [catch $::cmd msg] $msg + } {1 SQLITE_MISUSE} +} +grp delete + +do_execsql_test 4.2.1 { + CREATE TABLE t3(a, b PRIMARY KEY); +} +do_test 4.2 { + sqlite3changegroup grp + grp schema db main + grp change_begin INSERT t3 0 + + list [catch { grp change_begin INSERT t3 0 } msg] $msg +} {1 SQLITE_MISUSE} +grp delete + +do_test 4.3 { + sqlite3changegroup grp + grp schema db main + grp change_begin UPDATE t3 0 +} {} +foreach {tn cmd} { + 1 { grp change_blob old -1 abcd } + 2 { grp change_text new 2 helloworld } +} { + do_test 4.4.$tn { + list [catch $::cmd msg] $msg + } {1 SQLITE_RANGE} +} +grp delete + finish_test diff --git a/ext/session/sessionfault3.test b/ext/session/sessionfault3.test index 5af9c9ed70..f2bcb89417 100644 --- a/ext/session/sessionfault3.test +++ b/ext/session/sessionfault3.test @@ -43,7 +43,7 @@ set C2 [changeset_from_sql { INSERT INTO t1 VALUES(0, 0, 0, 0); }] -do_faultsim_test 1 -faults oom* -prep { +do_faultsim_test 1.2 -faults oom* -prep { sqlite3changegroup G } -body { G schema db main @@ -56,6 +56,20 @@ do_faultsim_test 1 -faults oom* -prep { faultsim_test_result {0 {}} {1 SQLITE_NOMEM} } +do_faultsim_test 1.3 -faults oom* -prep { + sqlite3changegroup G +} -body { + G schema db main + G change_begin INSERT t1 0 + G change_int64 new 0 12345 + G change_int64 new 1 67890 + G output + set {} {} +} -test { + catch { G delete } + faultsim_test_result {0 {}} {1 SQLITE_NOMEM} +} + #------------------------------------------------------------------------- reset_db do_execsql_test 2.0 { diff --git a/ext/session/test_session.c b/ext/session/test_session.c index 03929284db..a6d94ca5f3 100644 --- a/ext/session/test_session.c +++ b/ext/session/test_session.c @@ -1557,21 +1557,23 @@ static int SQLITE_TCLAPI test_changegroup_cmd( int nArg; const char *zMsg; } aSub[] = { - { "schema", 2, "DB DBNAME" }, /* 0 */ - { "add", 1, "CHANGESET" }, /* 1 */ - { "output", 0, "" }, /* 2 */ - { "delete", 0, "" }, /* 3 */ - { "add_change", 1, "ITERATOR" }, /* 4 */ + { "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 */ + { "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 */ - { "config", 2, "OPTION INTVAL" }, /* 12 */ + { "config", 2, "OPTION INTVAL" }, /* 12 */ + { "change_text-1", 3, "[new|old] ICOL VALUE" }, /* 13 */ + { "change_begin_ne", 3, "TYPE TABLE INDIRECT" }, /* 14 */ { 0, 0, 0 } }; int rc = TCL_OK; @@ -1641,7 +1643,8 @@ static int SQLITE_TCLAPI test_changegroup_cmd( break; }; - case 5: { /* change_begin */ + case 14: /* change_beginne */ + case 5: { /* change_begin */ struct ChangeType { const char *zType; int eType; @@ -1656,6 +1659,7 @@ static int SQLITE_TCLAPI test_changegroup_cmd( int bIndirect; int iIdx = 0; char *zErr = 0; + char **pz = ((iSub==5) ? &zErr : 0); if( TCL_OK!=Tcl_GetIntFromObj(0, objv[2], &eType) ){ rc = Tcl_GetIndexFromObjStruct( @@ -1669,7 +1673,7 @@ static int SQLITE_TCLAPI test_changegroup_cmd( return TCL_ERROR; } - rc = sqlite3changegroup_change_begin(p->pGrp, eType,zTab,bIndirect,&zErr); + rc = sqlite3changegroup_change_begin(p->pGrp, eType, zTab, bIndirect, pz); assert( zErr==0 || rc!=SQLITE_OK ); if( rc!=SQLITE_OK ){ rc = test_session_error(interp, rc, zErr); @@ -1809,6 +1813,23 @@ static int SQLITE_TCLAPI test_changegroup_cmd( break; } + case 13: { /* change_text-1 */ + 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{ + const char *pVal = Tcl_GetString(objv[4]); + rc = sqlite3changegroup_change_text(p->pGrp, bNew, iCol, pVal, -1); + if( rc!=SQLITE_OK ){ + rc = test_session_error(interp, rc, 0); + } + } + break; + } + default: { /* delete */ assert( iSub==3 ); Tcl_DeleteCommand(interp, Tcl_GetString(objv[0])); diff --git a/manifest b/manifest index c4852e1c4a..c093bdcad2 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Allow\sthe\sAPIs\son\sthis\sbranch\sto\sbe\sused\sto\screate\spatchsets\sas\swell\sas\schangesets. -D 2026-01-13T19:59:47.818 +C Add\stests\sfor\sedge\scases\sin\sthe\snew\scode\son\sthis\sbranch. +D 2026-01-14T15:27:26.954 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -562,12 +562,12 @@ 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 601d8151b7faeef300355b9eaea65c26d823746e83ccaa8d674198a12d6641dd +F ext/session/sessionchange2.test 6b5b7e3d1cc4ede43817f7fb68e3771aac4aa6500adb21a458b3e5a9fd841f83 F ext/session/sessionconflict.test 19e4a53795c4c930bfec49e809311e09b2a9e202d9446e56d7a8b139046a0c07 x F ext/session/sessiondiff.test e89f7aedcdd89e5ebac3a455224eb553a171e9586fc3e1e6a7b3388d2648ba8d F ext/session/sessionfault.test c2b43d01213b389a3f518e90775fca2120812ba51e50444c4066962263e45c11 F ext/session/sessionfault2.test b0d6a7c1d7398a7e800d84657404909c7d385965ea8576dc79ed344c46fbf41c -F ext/session/sessionfault3.test ce0b5d182133935c224d72507dbf1c5be1a1febf7e85d0b0fbd6d2f724b32b96 +F ext/session/sessionfault3.test 9397819ec25b0960c5bc03c78613f9cb5cacc970f83e817aec1775c2a839a787 F ext/session/sessioninvert.test 9018f6a7387ac745084b6374c5e1aa14d648b372e6e1181cfab3df632b662d26 x F ext/session/sessionmem.test f2a735db84a3e9e19f571033b725b0b2daf847f3f28b1da55a0c1a4e74f1de09 F ext/session/sessionnoact.test 4c7ae5c7d351cb5323bca62b6b095592ad24bd90a6713c178b62ab0063d23e19 @@ -580,7 +580,7 @@ F ext/session/sessionstat1.test 5e718d5888c0c49bbb33a7a4f816366db85f59f6a4f97544 F ext/session/sessionwor.test 6fd9a2256442cebde5b2284936ae9e0d54bde692d0f5fd009ecef8511f4cf3fc F ext/session/sqlite3session.c 16c36e63d41e2d0f6a8d4593618f221185064ab01d49b8514cb5e710642bdb67 F ext/session/sqlite3session.h cb6d25b573751afd9668f9ae8f1c7819df30916f43cc4c06b57749a6c2ce3e10 -F ext/session/test_session.c c4164b818cbdfaa3ebbcde31a9a620a63478478154715276ce09bced8918429b +F ext/session/test_session.c dae76c3da7eb51bc500451a37d23382f0d63f169927c5a26aee1db5a7dd6d91a F ext/wasm/GNUmakefile c3d007dd181527283d8674c812cc60518353f1f69c9a9d3008f10f53cea4a3c1 F ext/wasm/README-dist.txt f01081a850ce38a56706af6b481e3a7878e24e42b314cfcd4b129f0f8427066a F ext/wasm/README.md 2e87804e12c98f1d194b7a06162a88441d33bb443efcfe00dc6565a780d2f259 @@ -2192,8 +2192,8 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee F tool/warnings.sh d924598cf2f55a4ecbc2aeb055c10bd5f48114793e7ba25f9585435da29e7e98 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c -P 27150a8c22dc5331efa9e97637eae3741682bc14ae993f0b7672ccc63c37f1f9 -R f6f93392cfb2ac7fdf4dfeb41e3a044d +P 8699fe5b24c8aeb5e552aa7f4995f2b50dd33d1febd23ec1c07c8fc72497da72 +R 05ca11675af54a626e0a105c1338dfe9 U dan -Z b3689361f6f700b24a921b70fab2026e +Z f1ad619150ebc2af383c65f82ab9b53f # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 4b3655ff91..a7cf753e34 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -8699fe5b24c8aeb5e552aa7f4995f2b50dd33d1febd23ec1c07c8fc72497da72 +cd115f0f4437bf254058e40d70ba4e82e87227d2752695aa18f8431c3abc5c0b From c1975b6a6f3df98733bf36325b29148d42fced01 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 14 Jan 2026 17:02:22 +0000 Subject: [PATCH 4/4] Improve some of the documentation comments for the new code on this branch. FossilOrigin-Name: f352485db45a1280f91ec76f661b6dd50bb1d564b74bdff474c92e928860a47d --- ext/session/sqlite3session.c | 16 ++++++-------- ext/session/sqlite3session.h | 43 ++++++++++++++++++++---------------- manifest | 14 ++++++------ manifest.uuid | 2 +- 4 files changed, 39 insertions(+), 36 deletions(-) diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c index a16254523c..2b0f5581ea 100644 --- a/ext/session/sqlite3session.c +++ b/ext/session/sqlite3session.c @@ -7036,7 +7036,7 @@ int sqlite3changegroup_change_text( } /* -** Configure the change currently under construction with a text value. +** Configure the change currently under construction with a blob value. */ int sqlite3changegroup_change_blob( sqlite3_changegroup *pGrp, @@ -7061,6 +7061,9 @@ int sqlite3changegroup_change_blob( return SQLITE_OK; } +/* +** Finish any change currently being constructed by the changegroup object. +*/ int sqlite3changegroup_change_finish( sqlite3_changegroup *pGrp, int bDiscard, @@ -7149,14 +7152,9 @@ int sqlite3changegroup_change_finish( } if( rc==SQLITE_OK ){ 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 + pGrp, pGrp->cd.pTab, + pGrp->cd.eOp, pGrp->cd.bIndirect, pGrp->cd.pTab->nCol, + pGrp->cd.record.aBuf, pGrp->cd.record.nBuf, 0 ); } } diff --git a/ext/session/sqlite3session.h b/ext/session/sqlite3session.h index 36dfc504e9..a5083e7706 100644 --- a/ext/session/sqlite3session.h +++ b/ext/session/sqlite3session.h @@ -1856,7 +1856,7 @@ int sqlite3session_config(int op, void *pArg); /* ** CAPI3REF: Configure a changegroup object ** -** Configure the changegroup object passes as the first argument. +** Configure the changegroup object passed as the first argument. ** At present the only valid value for the second parameter is ** [SQLITE_CHANGEGROUP_CONFIG_PATCHSET]. */ @@ -1865,12 +1865,12 @@ int sqlite3changegroup_config(sqlite3_changegroup*, int, void *pArg); /* ** CAPI3REF: Options for sqlite3changegroup_config(). ** -** The following values may passed as the the 2nd parameter to -** sqlite3session_object_config(). +** The following values may be passed as the 2nd parameter to +** sqlite3changegroup_config(). ** **
SQLITE_CHANGEGROUP_CONFIG_PATCHSET
** A changegroup object generates either a changeset or patchset. Usually, -** which is determined by whether the first call to sqlite3changegroup_add() +** this is determined by whether the first call to sqlite3changegroup_add() ** is passed a changeset or a patchset. Or, if the first changes are added ** to the changegroup object using the sqlite3changegroup_change_xxx() ** APIs, then this option may be used to configure whether the changegroup @@ -1880,8 +1880,8 @@ int sqlite3changegroup_config(sqlite3_changegroup*, int, void *pArg); ** type int. If the changegroup currently contains zero changes, and the ** value of the int variable is zero or greater than zero, then the ** changegroup is configured to generate a changeset or patchset, -** respectively. It is not an error if the changegroup is not configured -** because it has already started accumuting changes, just a no-op. +** respectively. It is a no-op, not an error, if the changegroup is not +** configured because it has already started accumulating changes. ** ** Before returning, the int variable is set to 0 if the changegroup is ** configured to generate a changeset, or 1 if it is configured to generate @@ -1917,7 +1917,7 @@ int sqlite3changegroup_config(sqlite3_changegroup*, int, void *pArg); ** 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 +** called again on the same changegroup object until after ** sqlite3changegroup_change_finish() has been called. Doing so is an ** SQLITE_MISUSE error. ** @@ -1928,7 +1928,11 @@ int sqlite3changegroup_config(sqlite3_changegroup*, int, void *pArg); ** 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. +** If successful, SQLITE_OK is returned. Otherwise, if an error occurs, an +** SQLite error code is returned. In this case, if argument pzErr is non-NULL, +** then (*pzErr) may be set to point to a buffer containing a utf-8 formated, +** nul-terminated, English language error message. It is the responsibility +** of the caller to eventually free this buffer using sqlite3_free(). */ int sqlite3changegroup_change_begin( sqlite3_changegroup*, @@ -1939,7 +1943,7 @@ int sqlite3changegroup_change_begin( ); /* -** This function may only be called between a succesful call to +** This function may only be called between a successful 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 @@ -1964,8 +1968,8 @@ int sqlite3changegroup_change_begin( ** 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. +** If this call is successful, SQLITE_OK is returned. Otherwise, if an +** error occurs, an SQLite error code is returned. */ int sqlite3changegroup_change_int64( sqlite3_changegroup*, @@ -2018,7 +2022,7 @@ int sqlite3changegroup_change_blob( ** 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 +** If parameter bDiscard is zero, then an attempt is made to add the current ** change to the changegroup. Assuming the changegroup is configured to ** produce a changeset (not a patchset), this requires that: ** @@ -2038,10 +2042,11 @@ int sqlite3changegroup_change_blob( ** ** * 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(). +** 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 in the same way as for sqlite3changegroup_add(). ** ** For a patchset, all of the above rules apply except that it doesn't matter ** whether or not values are provided for the non-PK old.* record columns @@ -2052,9 +2057,9 @@ int sqlite3changegroup_change_blob( ** 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(). +** containing a nul-terminated, utf-8 encoded, English language error message. +** It is the responsibility of the caller to eventually free any such error +** message buffer using sqlite3_free(). */ int sqlite3changegroup_change_finish( sqlite3_changegroup*, diff --git a/manifest b/manifest index acfd050ffd..85952abefe 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Merge\slatest\strunk\schanges\sinto\sthis\sbranch. -D 2026-01-14T16:01:28.703 +C Improve\ssome\sof\sthe\sdocumentation\scomments\sfor\sthe\snew\scode\son\sthis\sbranch. +D 2026-01-14T17:02:22.575 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -578,8 +578,8 @@ 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 cad71a74f104fcdfc8eefaf544ea29f54c46a9a4d1a095b40d04824aa606f74a -F ext/session/sqlite3session.h cb6d25b573751afd9668f9ae8f1c7819df30916f43cc4c06b57749a6c2ce3e10 +F ext/session/sqlite3session.c 506f159483631ebe8885c304a59303f5c092ae92a915a7cb1cc2dc8a8acbced0 +F ext/session/sqlite3session.h b117cddb6ce93411b3b25c8a355d155a120e30efbdf506fc6899b71a333b0c56 F ext/session/test_session.c dae76c3da7eb51bc500451a37d23382f0d63f169927c5a26aee1db5a7dd6d91a F ext/wasm/GNUmakefile c3d007dd181527283d8674c812cc60518353f1f69c9a9d3008f10f53cea4a3c1 F ext/wasm/README-dist.txt f01081a850ce38a56706af6b481e3a7878e24e42b314cfcd4b129f0f8427066a @@ -2192,8 +2192,8 @@ F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee F tool/warnings.sh d924598cf2f55a4ecbc2aeb055c10bd5f48114793e7ba25f9585435da29e7e98 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f F tool/winmain.c 00c8fb88e365c9017db14c73d3c78af62194d9644feaf60e220ab0f411f3604c -P cd115f0f4437bf254058e40d70ba4e82e87227d2752695aa18f8431c3abc5c0b ed17a878e5a2e0cd1e9b69d528f5ac2ba8452d7c83deaf3cc72ecbff054f5ca3 -R f8b5009cc483f181e78204d06b24b034 +P fae9f376b74ae688dbdb5c0838a38427258d3d14c29c5346a3549e56db79961a +R b459bebcbcf7c8cf9788b1d94cb046aa U dan -Z fa614e954b104f0a1194ef8e1642c727 +Z 93999e32bda598fc2b940ad54d21c74f # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 40aaf4dc5c..6f97ccae01 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -fae9f376b74ae688dbdb5c0838a38427258d3d14c29c5346a3549e56db79961a +f352485db45a1280f91ec76f661b6dd50bb1d564b74bdff474c92e928860a47d