aboutsummaryrefslogtreecommitdiff
path: root/ext/session
diff options
context:
space:
mode:
authordan <Dan Kennedy>2021-04-21 20:52:17 +0000
committerdan <Dan Kennedy>2021-04-21 20:52:17 +0000
commita23a873fbb02bdb20923aae7d2476f07d4245bc3 (patch)
treef4b23113105b11a0e06da5f56a145cc0f2e24521 /ext/session
parent7437c25b63be5cdf678dc5d2f25d1837c126c8b7 (diff)
downloadsqlite-a23a873fbb02bdb20923aae7d2476f07d4245bc3.tar.gz
sqlite-a23a873fbb02bdb20923aae7d2476f07d4245bc3.zip
Add the experimental sqlite3session_changeset_size() API.
FossilOrigin-Name: b5564a6fd54875db1de884fdc0e5eeabcd6aa5595ad03a8a60843503e830a2d8
Diffstat (limited to 'ext/session')
-rw-r--r--ext/session/sessionsize.test108
-rw-r--r--ext/session/sqlite3session.c143
-rw-r--r--ext/session/sqlite3session.h11
-rw-r--r--ext/session/test_session.c7
4 files changed, 255 insertions, 14 deletions
diff --git a/ext/session/sessionsize.test b/ext/session/sessionsize.test
new file mode 100644
index 000000000..8347097e9
--- /dev/null
+++ b/ext/session/sessionsize.test
@@ -0,0 +1,108 @@
+# 2021 April 22
+#
+# 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 sessionsize
+
+proc do_changeset_size_test {tn sql} {
+ sqlite3session S db main
+ S attach *
+ db eval $sql
+
+ set sz [S changeset_size]
+ set C [S changeset]
+ set szC [string length $C]
+ S delete
+
+ do_test $tn "expr $sz" $szC
+}
+
+do_execsql_test 1.0 {
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c);
+ INSERT INTO t1 VALUES(1, 'abc', 'def');
+ INSERT INTO t1 VALUES(2, 'ghi', 'jkl');
+}
+
+do_changeset_size_test 1.1 {
+ INSERT INTO t1 VALUES(3, 'hello', 'world');
+}
+
+do_changeset_size_test 1.2 {
+ DELETE FROM t1 WHERE a=2;
+}
+
+do_changeset_size_test 1.3 {
+ DELETE FROM t1 WHERE a=3;
+ INSERT INTO t1 VALUES(3, 1, 2);
+}
+
+do_changeset_size_test 1.4 {
+ UPDATE t1 SET c='hello world' WHERE a=3;
+}
+
+#-------------------------------------------------------------------------
+
+do_execsql_test 2.0 {
+ CREATE TABlE t2(a, b, c, d, PRIMARY KEY(a, b)) WITHOUT ROWID;
+ CREATE TABlE t3(a, b, c, d PRIMARY KEY);
+}
+
+do_changeset_size_test 2.1 {
+ WITH s(i) AS (
+ SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<50
+ )
+ INSERT INTO t2 SELECT i, i+1, i+2, i+3 FROM s;
+
+ UPDATE t2 SET c=randomblob(a) WHERE a>10
+}
+
+do_changeset_size_test 2.2 {
+ DELETE FROM t2 WHERE a=1;
+ INSERT INTO t2 VALUES(1, 4, 3, 4);
+}
+
+do_changeset_size_test 2.2 {
+ UPDATE t2 SET b=4 WHERE a=2
+}
+
+do_changeset_size_test 2.3 {
+ INSERT INTO t2 VALUES('a', 'b', 'c', 'd');
+ UPDATE t2 SET c='qwertyuiop' WHERE a='a';
+}
+
+do_changeset_size_test 2.4 {
+ DELETE FROM t2 WHERE a='a';
+ INSERT INTO t2 VALUES('a', 'b', 'c', 'd');
+}
+
+do_changeset_size_test 2.5 {
+ UPDATE t2 SET a='aa', b='bb' WHERE (a, b) = ('a', 'b');
+}
+
+do_changeset_size_test 2.6 {
+ UPDATE t2 SET a='a', b='b' WHERE (a, b) = ('aa', 'bb');
+}
+
+do_changeset_size_test 2.7 {
+ INSERT INTO t3 DEFAULT VALUES;
+ INSERT INTO t3 VALUES(1,2,3,4);
+}
+
+finish_test
+
diff --git a/ext/session/sqlite3session.c b/ext/session/sqlite3session.c
index 8210056a8..077a6e090 100644
--- a/ext/session/sqlite3session.c
+++ b/ext/session/sqlite3session.c
@@ -49,6 +49,7 @@ struct sqlite3_session {
void *pFilterCtx; /* First argument to pass to xTableFilter */
int (*xTableFilter)(void *pCtx, const char *zTab);
i64 nMalloc; /* Number of bytes of data allocated */
+ i64 nMaxChangesetSize;
sqlite3_value *pZeroBlob; /* Value containing X'' */
sqlite3_session *pNext; /* Next session object on same db. */
SessionTable *pTable; /* List of attached tables */
@@ -291,8 +292,9 @@ struct SessionTable {
** this structure stored in a SessionTable.aChange[] hash table.
*/
struct SessionChange {
- int op; /* One of UPDATE, DELETE, INSERT */
- int bIndirect; /* True if this change is "indirect" */
+ u8 op; /* One of UPDATE, DELETE, INSERT */
+ u8 bIndirect; /* True if this change is "indirect" */
+ int nMaxSize; /* Max size of eventual changeset record */
int nRecord; /* Number of bytes in buffer aRecord[] */
u8 *aRecord; /* Buffer containing old.* record */
SessionChange *pNext; /* For hash-table collisions */
@@ -1121,6 +1123,10 @@ static int sessionInitTable(sqlite3_session *pSession, SessionTable *pTab){
if( 0==sqlite3_stricmp("sqlite_stat1", pTab->zName) ){
pTab->bStat1 = 1;
}
+
+ pSession->nMaxChangesetSize += (
+ 1 + sessionVarintLen(pTab->nCol) + pTab->nCol + strlen(pTab->zName) + 1
+ );
}
}
return (pSession->rc || pTab->abPK==0);
@@ -1166,6 +1172,103 @@ static int sessionStat1Depth(void *pCtx){
return p->hook.xDepth(p->hook.pCtx);
}
+static int sessionUpdateMaxSize(
+ int op,
+ sqlite3_session *pSession, /* Session object pTab is attached to */
+ SessionTable *pTab, /* Table that change applies to */
+ SessionChange *pC /* Update pC->nMaxSize */
+){
+ i64 nNew = 2;
+ if( pC->op==SQLITE_INSERT ){
+ if( op!=SQLITE_DELETE ){
+ int ii;
+ for(ii=0; ii<pTab->nCol; ii++){
+ sqlite3_value *p = 0;
+ pSession->hook.xNew(pSession->hook.pCtx, ii, &p);
+ sessionSerializeValue(0, p, &nNew);
+ }
+ }
+ }else if( op==SQLITE_DELETE ){
+ nNew += pC->nRecord;
+ if( sqlite3_preupdate_blobwrite(pSession->db)>=0 ){
+ nNew += pC->nRecord;
+ }
+ }else{
+ int ii;
+ u8 *pCsr = pC->aRecord;
+ for(ii=0; ii<pTab->nCol; ii++){
+ int bChanged = 1;
+ int nOld = 0;
+ int eType;
+ sqlite3_value *p = 0;
+ pSession->hook.xNew(pSession->hook.pCtx, ii, &p);
+ if( p==0 ){
+ return SQLITE_NOMEM;
+ }
+
+ eType = *pCsr++;
+ switch( eType ){
+ case SQLITE_NULL:
+ bChanged = sqlite3_value_type(p)!=SQLITE_NULL;
+ break;
+
+ case SQLITE_FLOAT:
+ case SQLITE_INTEGER: {
+ if( eType==sqlite3_value_type(p) ){
+ sqlite3_int64 iVal = sessionGetI64(pCsr);
+ if( eType==SQLITE_INTEGER ){
+ bChanged = (iVal!=sqlite3_value_int64(p));
+ }else{
+ double dVal;
+ memcpy(&dVal, &iVal, 8);
+ bChanged = (dVal!=sqlite3_value_double(p));
+ }
+ }
+ nOld = 8;
+ pCsr += 8;
+ break;
+ }
+
+ default: {
+ int nByte;
+ nOld = sessionVarintGet(pCsr, &nByte);
+ pCsr += nOld;
+ nOld += nByte;
+ assert( eType==SQLITE_TEXT || eType==SQLITE_BLOB );
+ if( eType==sqlite3_value_type(p)
+ && nByte==sqlite3_value_bytes(p)
+ && (nByte==0 || 0==memcmp(pCsr, sqlite3_value_blob(p), nByte))
+ ){
+ bChanged = 0;
+ }
+ pCsr += nByte;
+ break;
+ }
+ }
+
+ if( bChanged && pTab->abPK[ii] ){
+ nNew = pC->nRecord + 2;
+ break;
+ }
+
+ if( bChanged ){
+ nNew += 1 + nOld;
+ sessionSerializeValue(0, p, &nNew);
+ }else if( pTab->abPK[ii] ){
+ nNew += 2 + nOld;
+ }else{
+ nNew += 2;
+ }
+ }
+ }
+
+ if( nNew>pC->nMaxSize ){
+ int nIncr = nNew - pC->nMaxSize;
+ pC->nMaxSize = nNew;
+ pSession->nMaxChangesetSize += nIncr;
+ }
+ return SQLITE_OK;
+}
/*
** This function is only called from with a pre-update-hook reporting a
@@ -1239,7 +1342,6 @@ static void sessionPreupdateOneChange(
/* Create a new change object containing all the old values (if
** this is an SQLITE_UPDATE or SQLITE_DELETE), or just the PK
** values (if this is an INSERT). */
- SessionChange *pChange; /* New change object */
sqlite3_int64 nByte; /* Number of bytes to allocate */
int i; /* Used to iterate through columns */
@@ -1265,13 +1367,13 @@ static void sessionPreupdateOneChange(
}
/* Allocate the change object */
- pChange = (SessionChange *)sessionMalloc64(pSession, nByte);
- if( !pChange ){
+ pC = (SessionChange *)sessionMalloc64(pSession, nByte);
+ if( !pC ){
rc = SQLITE_NOMEM;
goto error_out;
}else{
- memset(pChange, 0, sizeof(SessionChange));
- pChange->aRecord = (u8 *)&pChange[1];
+ memset(pC, 0, sizeof(SessionChange));
+ pC->aRecord = (u8 *)&pC[1];
}
/* Populate the change object. None of the preupdate_old(),
@@ -1286,17 +1388,17 @@ static void sessionPreupdateOneChange(
}else if( pTab->abPK[i] ){
pSession->hook.xNew(pSession->hook.pCtx, i, &p);
}
- sessionSerializeValue(&pChange->aRecord[nByte], p, &nByte);
+ sessionSerializeValue(&pC->aRecord[nByte], p, &nByte);
}
/* Add the change to the hash-table */
if( pSession->bIndirect || pSession->hook.xDepth(pSession->hook.pCtx) ){
- pChange->bIndirect = 1;
+ pC->bIndirect = 1;
}
- pChange->nRecord = nByte;
- pChange->op = op;
- pChange->pNext = pTab->apChange[iHash];
- pTab->apChange[iHash] = pChange;
+ pC->nRecord = nByte;
+ pC->op = op;
+ pC->pNext = pTab->apChange[iHash];
+ pTab->apChange[iHash] = pC;
}else if( pC->bIndirect ){
/* If the existing change is considered "indirect", but this current
@@ -1307,8 +1409,12 @@ static void sessionPreupdateOneChange(
pC->bIndirect = 0;
}
}
+
+ assert( rc==SQLITE_OK );
+ rc = sessionUpdateMaxSize(op, pSession, pTab, pC);
}
+
/* If an error has occurred, mark the session object as failed. */
error_out:
if( pTab->bStat1 ){
@@ -2520,7 +2626,9 @@ int sqlite3session_changeset(
int *pnChangeset, /* OUT: Size of buffer at *ppChangeset */
void **ppChangeset /* OUT: Buffer containing changeset */
){
- return sessionGenerateChangeset(pSession, 0, 0, 0, pnChangeset, ppChangeset);
+ int rc = sessionGenerateChangeset(pSession, 0, 0, 0, pnChangeset,ppChangeset);
+ assert( rc || pnChangeset==0 || *pnChangeset<=pSession->nMaxChangesetSize );
+ return rc;
}
/*
@@ -2613,6 +2721,13 @@ sqlite3_int64 sqlite3session_memory_used(sqlite3_session *pSession){
}
/*
+** Return the maximum size of sqlite3session_changeset() output.
+*/
+sqlite3_int64 sqlite3session_changeset_size(sqlite3_session *pSession){
+ return pSession->nMaxChangesetSize;
+}
+
+/*
** Do the work for either sqlite3changeset_start() or start_strm().
*/
static int sessionChangesetStart(
diff --git a/ext/session/sqlite3session.h b/ext/session/sqlite3session.h
index 9ebf360ea..78d25ba0f 100644
--- a/ext/session/sqlite3session.h
+++ b/ext/session/sqlite3session.h
@@ -325,6 +325,17 @@ int sqlite3session_changeset(
/*
** CAPI3REF: Load The Difference Between Tables Into A Session
+** METHOD: sqlite3session_changeset_size()
+**
+** This function returns an upper limit, in bytes, of the size of the
+** changeset that might be produced if sqlite3session_changeset() were
+** called. The final changeset size might be equal to or smaller than
+** the size in bytes returned by this function.
+*/
+sqlite3_int64 sqlite3session_changeset_size(sqlite3_session *pSession);
+
+/*
+** CAPI3REF: Load The Difference Between Tables Into A Session
** METHOD: sqlite3_session
**
** If it is not already attached to the session object passed as the first
diff --git a/ext/session/test_session.c b/ext/session/test_session.c
index c3898fd86..b7c96b265 100644
--- a/ext/session/test_session.c
+++ b/ext/session/test_session.c
@@ -246,6 +246,7 @@ static int SQLITE_TCLAPI test_session_cmd(
{ "patchset", 0, "", }, /* 7 */
{ "diff", 2, "FROMDB TBL", }, /* 8 */
{ "memory_used", 0, "", }, /* 9 */
+ { "changeset_size", 0, "", }, /* 10 */
{ 0 }
};
int iSub;
@@ -357,6 +358,12 @@ static int SQLITE_TCLAPI test_session_cmd(
Tcl_SetObjResult(interp, Tcl_NewWideIntObj(nMalloc));
break;
}
+
+ case 10: {
+ sqlite3_int64 nSize = sqlite3session_changeset_size(pSession);
+ Tcl_SetObjResult(interp, Tcl_NewWideIntObj(nSize));
+ break;
+ }
}
return TCL_OK;