From: dan Date: Mon, 12 Jan 2026 16:39:27 +0000 (+0000) Subject: Add an API to the sessions module to add changes one at a time to an sqlite3_changegr... X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=817c5093652b3e09da225d2ea30d9db1c39b9e8e;p=thirdparty%2Fsqlite.git Add an API to the sessions module to add changes one at a time to an sqlite3_changegroup object. FossilOrigin-Name: 27150a8c22dc5331efa9e97637eae3741682bc14ae993f0b7672ccc63c37f1f9 --- 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