--- /dev/null
+# 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
+
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).
/* 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;
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;
}
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 ){
);
}
+/*
+** 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.
*/
sqlite3 *db; /* Configured by changegroup_schema() */
char *zDb; /* Configured by changegroup_schema() */
+ ChangeData cd; /* Used by changegroup_change_xxx() APIs. */
};
/*
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;
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;
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;
}
}
/* Check that the table is compatible. */
- if( !sessionChangesetCheckCompat(pTab, nCol, abPK) ){
+ if( pIter && !sessionChangesetCheckCompat(pTab, nCol, abPK) ){
rc = SQLITE_SCHEMA;
}
}
/*
-** Add the change currently indicated by iterator pIter to the hash table
-** belonging to changegroup pGrp.
+** Add a single change to the changegroup pGrp.
*/
static int sessionOneChangeToHash(
- sqlite3_changegroup *pGrp,
- sqlite3_changeset_iter *pIter,
- int bRebase
+ sqlite3_changegroup *pGrp, /* Changegroup to update */
+ SessionTable *pTab, /* Table change pertains to */
+ int op, /* One of SQLITE_INSERT, UPDATE, DELETE */
+ int bIndirect, /* True to flag change as "indirect" */
+ int nCol, /* Number of columns in record(s) */
+ u8 *aRec, /* Serialized change record(s) */
+ int nRec, /* Size of aRec[] in bytes */
+ int bRebase /* True if this is a rebase blob */
){
int rc = SQLITE_OK;
- int nCol = 0;
- int op = 0;
int iHash = 0;
- int bIndirect = 0;
SessionChange *pChange = 0;
SessionChange *pExist = 0;
SessionChange **pp = 0;
- SessionTable *pTab = 0;
- u8 *aRec = &pIter->in.aData[pIter->in.iCurrent + 2];
- int nRec = (pIter->in.iNext - pIter->in.iCurrent) - 2;
assert( nRec>0 );
- /* Ensure that only changesets, or only patchsets, but not a mixture
- ** of both, are being combined. It is an error to try to combine a
- ** changeset and a patchset. */
- if( pGrp->pList==0 ){
- pGrp->bPatch = pIter->bPatchset;
- }else if( pIter->bPatchset!=pGrp->bPatch ){
- rc = SQLITE_ERROR;
- }
-
- if( rc==SQLITE_OK ){
- const char *zTab = 0;
- sqlite3changeset_op(pIter, &zTab, &nCol, &op, &bIndirect);
- rc = sessionChangesetFindTable(pGrp, zTab, pIter, &pTab);
- }
-
- if( rc==SQLITE_OK && nCol<pTab->nCol ){
+ if( nCol<pTab->nCol ){
SessionBuffer *pBuf = &pGrp->rec;
rc = sessionChangesetExtendRecord(pGrp, pTab, nCol, op, aRec, nRec, pBuf);
aRec = pBuf->aBuf;
assert( pGrp->db );
}
- if( rc==SQLITE_OK && sessionGrowHash(0, pIter->bPatchset, pTab) ){
+ if( rc==SQLITE_OK && sessionGrowHash(0, pGrp->bPatch, pTab) ){
rc = SQLITE_NOMEM;
}
/* 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;
}
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 ){
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;
}
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;
}
rc = SQLITE_ERROR;
}else{
pIter->in.bNoDiscard = 1;
- rc = sessionOneChangeToHash(pGrp, pIter, 0);
+ rc = sessionOneChangeIterToHash(pGrp, pIter, 0);
}
return rc;
}
*/
void sqlite3changegroup_delete(sqlite3_changegroup *pGrp){
if( pGrp ){
+ int ii;
+ for(ii=0; ii<pGrp->cd.nBufAlloc; ii++){
+ sqlite3_free(pGrp->cd.aBuf[ii].aBuf);
+ }
+ sqlite3_free(pGrp->cd.record.aBuf);
+ sqlite3_free(pGrp->cd.aBuf);
sqlite3_free(pGrp->zDb);
sessionDeleteTable(0, pGrp->pList);
sqlite3_free(pGrp->rec.aBuf);
return rc;
}
+/*
+** Begin adding a change to a changegroup object.
+*/
+int sqlite3changegroup_change_begin(
+ sqlite3_changegroup *pGrp,
+ int eOp,
+ const char *zTab,
+ int bIndirect,
+ char **pzErr
+){
+ SessionTable *pTab = 0;
+ int rc = SQLITE_OK;
+
+ if( pGrp->cd.pTab ){
+ rc = SQLITE_MISUSE;
+ }else if( eOp!=SQLITE_INSERT && eOp!=SQLITE_UPDATE && eOp!=SQLITE_DELETE ){
+ rc = SQLITE_ERROR;
+ }else{
+ rc = sessionChangesetFindTable(pGrp, zTab, 0, &pTab);
+ }
+ if( rc==SQLITE_OK ){
+ if( pTab==0 ){
+ if( pzErr ){
+ *pzErr = sqlite3_mprintf("no such table: %s", zTab);
+ }
+ rc = SQLITE_ERROR;
+ }else{
+ int nReq = pTab->nCol * (eOp==SQLITE_UPDATE ? 2 : 1);
+ pGrp->cd.pTab = pTab;
+ pGrp->cd.eOp = eOp;
+ pGrp->cd.bIndirect = bIndirect;
+
+ if( pGrp->cd.nBufAlloc<nReq ){
+ SessionBuffer *aBuf = (SessionBuffer*)sqlite3_realloc(
+ pGrp->cd.aBuf, nReq * sizeof(SessionBuffer)
+ );
+ if( aBuf==0 ){
+ rc = SQLITE_NOMEM;
+ }else{
+ memset(&aBuf[pGrp->cd.nBufAlloc], 0,
+ sizeof(SessionBuffer) * (nReq - pGrp->cd.nBufAlloc)
+ );
+ pGrp->cd.aBuf = aBuf;
+ pGrp->cd.nBufAlloc = nReq;
+ }
+ }
+
+#ifdef SQLITE_DEBUG
+ {
+ /* Assert that all column values are currently undefined */
+ int ii;
+ for(ii=0; ii<pGrp->cd.nBufAlloc; ii++){
+ assert( pGrp->cd.aBuf[ii].nBuf==0 );
+ }
+ }
+#endif
+ }
+ }
+
+ return rc;
+}
+
+static int checkChangeParams(
+ sqlite3_changegroup *pGrp,
+ int bNew,
+ int iCol,
+ sqlite3_int64 nReq,
+ SessionBuffer **ppBuf
+){
+ int rc = SQLITE_OK;
+ if( pGrp->cd.pTab==0 ){
+ rc = SQLITE_MISUSE;
+ }else if( iCol<0 || iCol>=pGrp->cd.pTab->nCol ){
+ rc = SQLITE_RANGE;
+ }else if(
+ (bNew && pGrp->cd.eOp==SQLITE_DELETE)
+ || (!bNew && pGrp->cd.eOp==SQLITE_INSERT)
+ ){
+ rc = SQLITE_ERROR;
+ }else{
+ SessionBuffer *pBuf = &pGrp->cd.aBuf[iCol];
+ if( pGrp->cd.eOp==SQLITE_UPDATE && bNew ){
+ pBuf += pGrp->cd.pTab->nCol;
+ }
+ pBuf->nBuf = 0;
+ sessionBufferGrow(pBuf, nReq, &rc);
+ pBuf->nBuf = nReq;
+ *ppBuf = pBuf;
+ }
+ return rc;
+}
+
+/*
+** Configure the change currently under construction with an integer value.
+*/
+int sqlite3changegroup_change_int64(
+ sqlite3_changegroup *pGrp,
+ int bNew,
+ int iCol,
+ sqlite3_int64 iVal
+){
+ int rc = SQLITE_OK;
+ SessionBuffer *pBuf = 0;
+
+ if( SQLITE_OK!=(rc = checkChangeParams(pGrp, bNew, iCol, 9, &pBuf)) ){
+ return rc;
+ }
+
+ pBuf->aBuf[0] = SQLITE_INTEGER;
+ sessionPutI64(&pBuf->aBuf[1], iVal);
+ return SQLITE_OK;
+}
+
+/*
+** Configure the change currently under construction with a null value.
+*/
+int sqlite3changegroup_change_null(
+ sqlite3_changegroup *pGrp,
+ int bNew,
+ int iCol
+){
+ int rc = SQLITE_OK;
+ SessionBuffer *pBuf = 0;
+
+ if( SQLITE_OK!=(rc = checkChangeParams(pGrp, bNew, iCol, 1, &pBuf)) ){
+ return rc;
+ }
+
+ pBuf->aBuf[0] = SQLITE_NULL;
+ return SQLITE_OK;
+}
+
+/*
+** Configure the change currently under construction with a real value.
+*/
+int sqlite3changegroup_change_double(
+ sqlite3_changegroup *pGrp,
+ int bNew,
+ int iCol,
+ double fVal
+){
+ int rc = SQLITE_OK;
+ SessionBuffer *pBuf = 0;
+
+ if( SQLITE_OK!=(rc = checkChangeParams(pGrp, bNew, iCol, 9, &pBuf)) ){
+ return rc;
+ }
+
+ pBuf->aBuf[0] = SQLITE_FLOAT;
+ sessionPutDouble(&pBuf->aBuf[1], fVal);
+ return SQLITE_OK;
+}
+
+/*
+** Configure the change currently under construction with a text value.
+*/
+int sqlite3changegroup_change_text(
+ sqlite3_changegroup *pGrp,
+ int bNew,
+ int iCol,
+ const char *pVal,
+ int nVal
+){
+ int nText = nVal>=0 ? nVal : strlen(pVal);
+ sqlite3_int64 nByte = 1 + sessionVarintLen(nText) + nText;
+ int rc = SQLITE_OK;
+ SessionBuffer *pBuf = 0;
+
+ if( SQLITE_OK!=(rc = checkChangeParams(pGrp, bNew, iCol, nByte, &pBuf)) ){
+ return rc;
+ }
+
+ pBuf->aBuf[0] = SQLITE_TEXT;
+ pBuf->nBuf = (1 + sessionVarintPut(&pBuf->aBuf[1], nText));
+ memcpy(&pBuf->aBuf[pBuf->nBuf], pVal, nText);
+
+ return SQLITE_OK;
+}
+
+/*
+** Configure the change currently under construction with a text value.
+*/
+int sqlite3changegroup_change_blob(
+ sqlite3_changegroup *pGrp,
+ int bNew,
+ int iCol,
+ const void *pVal,
+ int nVal
+){
+ sqlite3_int64 nByte = 1 + sessionVarintLen(nVal) + nVal;
+ int rc = SQLITE_OK;
+ SessionBuffer *pBuf = 0;
+
+ if( SQLITE_OK!=(rc = checkChangeParams(pGrp, bNew, iCol, nByte, &pBuf)) ){
+ return rc;
+ }
+
+ pBuf->aBuf[0] = SQLITE_BLOB;
+ pBuf->nBuf = (1 + sessionVarintPut(&pBuf->aBuf[1], nVal));
+ memcpy(&pBuf->aBuf[pBuf->nBuf], pVal, nVal);
+
+ return SQLITE_OK;
+}
+
+int sqlite3changegroup_change_finish(
+ sqlite3_changegroup *pGrp,
+ int bDiscard,
+ char **pzErr
+){
+ int rc = SQLITE_OK;
+ if( pGrp->cd.pTab ){
+ SessionBuffer *aBuf = pGrp->cd.aBuf;
+ int ii;
+
+ if( bDiscard==0 ){
+ int nBuf = pGrp->cd.pTab->nCol;
+ int nRec = 0;
+ u8 eUndef = SQLITE_NULL;
+ if( pGrp->cd.eOp==SQLITE_UPDATE ){
+ for(ii=0; ii<nBuf; ii++){
+ if( pGrp->cd.pTab->abPK[ii] ){
+ if( aBuf[ii].nBuf<=1 ){
+ *pzErr = sqlite3_mprintf(
+ "invalid change: %s value in PK of old.* record",
+ aBuf[ii].nBuf==1 ? "null" : "undefined"
+ );
+ rc = SQLITE_ERROR;
+ break;
+ }else if( aBuf[ii + nBuf].nBuf>0 ){
+ *pzErr = sqlite3_mprintf(
+ "invalid change: defined value in PK of new.* record"
+ );
+ rc = SQLITE_ERROR;
+ break;
+ }
+ }else if( (aBuf[ii].nBuf>0)!=(aBuf[ii+nBuf].nBuf>0) ){
+ *pzErr = sqlite3_mprintf(
+ "invalid change: column %d "
+ "- old.* value is %sdefined but new.* is %sdefined",
+ ii, aBuf[ii].nBuf ? "" : "un", aBuf[ii+nBuf].nBuf ? "" : "un"
+ );
+ rc = SQLITE_ERROR;
+ break;
+ }
+ }
+ eUndef = 0x00;
+ nBuf = nBuf * 2;
+ }else{
+ for(ii=0; ii<nBuf; ii++){
+ if( aBuf[ii].nBuf==0 ){
+ *pzErr = sqlite3_mprintf(
+ "invalid change: column %d is undefined", ii
+ );
+ rc = SQLITE_ERROR;
+ break;
+ }
+ if( aBuf[ii].nBuf==1 && pGrp->cd.pTab->abPK[ii] ){
+ *pzErr = sqlite3_mprintf(
+ "invalid change: null value in PK"
+ );
+ rc = SQLITE_ERROR;
+ break;
+ }
+ }
+ }
+
+ for(ii=0; ii<nBuf; ii++){
+ SessionBuffer *pBuf = &pGrp->cd.aBuf[ii];
+ nRec += (pBuf->nBuf ? pBuf->nBuf : 1);
+ }
+ if( 0==sessionBufferGrow(&pGrp->cd.record, nRec, &rc) ){
+ for(ii=0; ii<nBuf; ii++){
+ SessionBuffer *p = &pGrp->cd.aBuf[ii];
+ if( p->nBuf ){
+ memcpy(&pGrp->cd.record.aBuf[pGrp->cd.record.nBuf],p->aBuf,p->nBuf);
+ pGrp->cd.record.nBuf += p->nBuf;
+ }else{
+ pGrp->cd.record.aBuf[pGrp->cd.record.nBuf++] = eUndef;
+ }
+ }
+ rc = sessionOneChangeToHash(
+ pGrp,
+ pGrp->cd.pTab,
+ pGrp->cd.eOp,
+ pGrp->cd.bIndirect,
+ pGrp->cd.pTab->nCol,
+ pGrp->cd.record.aBuf,
+ pGrp->cd.record.nBuf,
+ 0
+ );
+ }
+ }
+
+ {
+ int nZero = pGrp->cd.pTab->nCol;
+ if( pGrp->cd.eOp==SQLITE_UPDATE ) nZero += nZero;
+ for(ii=0; ii<nZero; ii++){
+ pGrp->cd.aBuf[ii].nBuf = 0;
+ }
+ }
+ pGrp->cd.pTab = 0;
+ }
+
+ return rc;
+}
+
#endif /* SQLITE_ENABLE_SESSION && SQLITE_ENABLE_PREUPDATE_HOOK */
*/
#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++.
*/
typedef unsigned char u8;
#endif
+extern const char *sqlite3ErrName(int);
+
typedef struct TestSession TestSession;
struct TestSession {
sqlite3_session *pSession;
}
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));
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
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;
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]));
-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
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
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
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.
-branch trunk
-tag trunk
+branch changegroup-change-api
+tag changegroup-change-api
-b57a8215f4259a0aae188b7ee5060f8ff48919303179aae80b58b43ed3b991f5
+27150a8c22dc5331efa9e97637eae3741682bc14ae993f0b7672ccc63c37f1f9