diff --git a/WORKSPACE b/WORKSPACE index f1b05575eba..587c11ef975 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -46,6 +46,10 @@ http_archive( strip_prefix = "sqlite-amalgamation-3400100", type = "zip", url = "https://sqlite.org/2022/sqlite-amalgamation-3400100.zip", + patches = [ + "//:patches/sqlite/0001-row-counts-amalgamation.patch", + ], + patch_args = ["-p1"], ) http_archive( diff --git a/patches/sqlite/0001-row-counts-amalgamation.patch b/patches/sqlite/0001-row-counts-amalgamation.patch new file mode 100644 index 00000000000..50d41a2221d --- /dev/null +++ b/patches/sqlite/0001-row-counts-amalgamation.patch @@ -0,0 +1,302 @@ +diff -u5 -r sqlite-amalgamation-pristine/shell.c sqlite-amalgamation-modified/shell.c +--- sqlite-amalgamation-pristine/shell.c 2022-12-28 06:26:39.000000000 -0800 ++++ sqlite-amalgamation-modified/shell.c 2023-08-04 14:53:19.663535828 -0700 +@@ -17356,10 +17356,15 @@ + raw_printf(pArg->out, "Reprepare operations: %d\n", iCur); + iCur = sqlite3_stmt_status(pArg->pStmt, SQLITE_STMTSTATUS_RUN, bReset); + raw_printf(pArg->out, "Number of times run: %d\n", iCur); + iCur = sqlite3_stmt_status(pArg->pStmt, SQLITE_STMTSTATUS_MEMUSED, bReset); + raw_printf(pArg->out, "Memory used by prepared stmt: %d\n", iCur); ++ ++ iCur = sqlite3_stmt_status(pArg->pStmt, LIBSQL_STMTSTATUS_ROWS_READ, bReset); ++ raw_printf(pArg->out, "Rows read: %d\n", iCur); ++ iCur = sqlite3_stmt_status(pArg->pStmt, LIBSQL_STMTSTATUS_ROWS_WRITTEN, bReset); ++ raw_printf(pArg->out, "Rows written: %d\n", iCur); + } + + #ifdef __linux__ + displayLinuxIoStats(pArg->out); + #endif +diff -u5 -r sqlite-amalgamation-pristine/sqlite3.c sqlite-amalgamation-modified/sqlite3.c +--- sqlite-amalgamation-pristine/sqlite3.c 2022-12-28 06:26:39.000000000 -0800 ++++ sqlite-amalgamation-modified/sqlite3.c 2023-08-04 14:53:19.667535836 -0700 +@@ -452,11 +452,11 @@ + ** [sqlite3_libversion_number()], [sqlite3_sourceid()], + ** [sqlite_version()] and [sqlite_source_id()]. + */ + #define SQLITE_VERSION "3.40.1" + #define SQLITE_VERSION_NUMBER 3040001 +-#define SQLITE_SOURCE_ID "2022-12-28 14:03:47 df5c253c0b3dd24916e4ec7cf77d3db5294cc9fd45ae7b9c5e82ad8197f38a24" ++#define SQLITE_SOURCE_ID "2022-12-28 14:03:47 df5c253c0b3dd24916e4ec7cf77d3db5294cc9fd45ae7b9c5e82ad8197f3alt1" + + /* + ** CAPI3REF: Run-Time Library Version Numbers + ** KEYWORDS: sqlite3_version sqlite3_sourceid + ** +@@ -8921,10 +8921,18 @@ + ** used to store the prepared statement. ^This value is not actually + ** a counter, and so the resetFlg parameter to sqlite3_stmt_status() + ** is ignored when the opcode is SQLITE_STMTSTATUS_MEMUSED. + ** + ** ++** ++** [[LIBSQL_STMTSTATUS_ROWS_READ]] ++** [[LIBSQL_STMTSTATUS_ROWS_WRITTEN]] ++**
LIBSQL_STMTSTATUS_ROWS_READ
++** LIBSQL_STMTSTATUS_ROWS_WRITTEN
++**
^LIBSQL_STMTSTATUS_ROWS_READ is the number of rows read when executing ++** this statement. LIBSQL_STMTSTATUS_ROWS_WRITTEN value is the number of ++** rows written. + */ + #define SQLITE_STMTSTATUS_FULLSCAN_STEP 1 + #define SQLITE_STMTSTATUS_SORT 2 + #define SQLITE_STMTSTATUS_AUTOINDEX 3 + #define SQLITE_STMTSTATUS_VM_STEP 4 +@@ -8932,10 +8940,14 @@ + #define SQLITE_STMTSTATUS_RUN 6 + #define SQLITE_STMTSTATUS_FILTER_MISS 7 + #define SQLITE_STMTSTATUS_FILTER_HIT 8 + #define SQLITE_STMTSTATUS_MEMUSED 99 + ++#define LIBSQL_STMTSTATUS_BASE 1024 ++#define LIBSQL_STMTSTATUS_ROWS_READ LIBSQL_STMTSTATUS_BASE + 1 ++#define LIBSQL_STMTSTATUS_ROWS_WRITTEN LIBSQL_STMTSTATUS_BASE + 2 ++ + /* + ** CAPI3REF: Custom Page Cache Object + ** + ** The sqlite3_pcache type is opaque. It is implemented by + ** the pluggable module. The SQLite core has no knowledge of +@@ -22778,10 +22790,11 @@ + bft readOnly:1; /* True for statements that do not write */ + bft bIsReader:1; /* True for statements that read */ + yDbMask btreeMask; /* Bitmask of db->aDb[] entries referenced */ + yDbMask lockMask; /* Subset of btreeMask that requires a lock */ + u32 aCounter[9]; /* Counters used by sqlite3_stmt_status() */ ++ u32 aLibsqlCounter[3]; /* libSQL extension: Counters used by sqlite3_stmt_status()*/ + char *zSql; /* Text of the SQL statement that generated this */ + #ifdef SQLITE_ENABLE_NORMALIZE + char *zNormSql; /* Normalization of the associated SQL statement */ + DblquoteStr *pDblStr; /* List of double-quoted string literals */ + #endif +@@ -89160,11 +89173,11 @@ + SQLITE_API int sqlite3_stmt_status(sqlite3_stmt *pStmt, int op, int resetFlag){ + Vdbe *pVdbe = (Vdbe*)pStmt; + u32 v; + #ifdef SQLITE_ENABLE_API_ARMOR + if( !pStmt +- || (op!=SQLITE_STMTSTATUS_MEMUSED && (op<0||op>=ArraySize(pVdbe->aCounter))) ++ || (op!=SQLITE_STMTSTATUS_MEMUSED && (op<0||(op>=ArraySize(pVdbe->aCounter)&&oplookaside.pEnd = db->lookaside.pStart; + sqlite3VdbeDelete(pVdbe); + db->pnBytesFreed = 0; + db->lookaside.pEnd = db->lookaside.pTrueEnd; + sqlite3_mutex_leave(db->mutex); ++ }else if( op>=LIBSQL_STMTSTATUS_BASE ){ ++ v = pVdbe->aLibsqlCounter[op - LIBSQL_STMTSTATUS_BASE]; ++ if( resetFlag ) pVdbe->aLibsqlCounter[op - LIBSQL_STMTSTATUS_BASE] = 0; + }else{ + v = pVdbe->aCounter[op]; + if( resetFlag ) pVdbe->aCounter[op] = 0; + } + return (int)v; +@@ -93285,10 +93301,11 @@ + if( pOp->p3 ){ + nEntry = sqlite3BtreeRowCountEst(pCrsr); + }else{ + nEntry = 0; /* Not needed. Only used to silence a warning. */ + rc = sqlite3BtreeCount(db, pCrsr, &nEntry); ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_READ - LIBSQL_STMTSTATUS_BASE] += nEntry; + if( rc ) goto abort_due_to_error; + } + pOut = out2Prerelease(p, pOp); + pOut->u.i = nEntry; + goto check_for_interrupt; +@@ -94444,10 +94461,11 @@ + if( eqOnly && r.eqSeen==0 ){ + assert( res!=0 ); + goto seek_not_found; + } + } ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_READ - LIBSQL_STMTSTATUS_BASE]++; + #ifdef SQLITE_TEST + sqlite3_search_count++; + #endif + if( oc>=OP_SeekGE ){ assert( oc==OP_SeekGE || oc==OP_SeekGT ); + if( res<0 || (res==0 && oc==OP_SeekGT) ){ +@@ -95012,10 +95030,11 @@ + pC->nullRow = 0; + pC->cacheStatus = CACHE_STALE; + pC->deferredMoveto = 0; + VdbeBranchTaken(res!=0,2); + pC->seekResult = res; ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_READ - LIBSQL_STMTSTATUS_BASE]++; + if( res!=0 ){ + assert( rc==SQLITE_OK ); + if( pOp->p2==0 ){ + rc = SQLITE_CORRUPT_BKPT; + }else{ +@@ -95268,10 +95287,11 @@ + } + } + if( pOp->p5 & OPFLAG_ISNOOP ) break; + #endif + ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_WRITTEN - LIBSQL_STMTSTATUS_BASE]++; + if( pOp->p5 & OPFLAG_NCHANGE ) p->nChange++; + if( pOp->p5 & OPFLAG_LASTROWID ) db->lastRowid = x.nKey; + assert( (pData->flags & (MEM_Blob|MEM_Str))!=0 || pData->n==0 ); + x.pData = pData->z; + x.nData = pData->n; +@@ -95451,10 +95471,11 @@ + pC->seekResult = 0; + if( rc ) goto abort_due_to_error; + + /* Invoke the update-hook if required. */ + if( opflags & OPFLAG_NCHANGE ){ ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_WRITTEN - LIBSQL_STMTSTATUS_BASE]++; + p->nChange++; + if( db->xUpdateCallback && ALWAYS(pTab!=0) && HasRowid(pTab) ){ + db->xUpdateCallback(db->pUpdateArg, SQLITE_DELETE, zDb, pTab->zName, + pC->movetoTarget); + assert( pC->iDb>=0 ); +@@ -95738,10 +95759,11 @@ + rc = sqlite3BtreeLast(pCrsr, &res); + pC->nullRow = (u8)res; + pC->deferredMoveto = 0; + pC->cacheStatus = CACHE_STALE; + if( rc ) goto abort_due_to_error; ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_READ - LIBSQL_STMTSTATUS_BASE]++; + if( pOp->p2>0 ){ + VdbeBranchTaken(res!=0,2); + if( res ) goto jump_to_p2; + } + break; +@@ -95842,10 +95864,11 @@ + pC->deferredMoveto = 0; + pC->cacheStatus = CACHE_STALE; + } + if( rc ) goto abort_due_to_error; + pC->nullRow = (u8)res; ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_READ - LIBSQL_STMTSTATUS_BASE]++; + assert( pOp->p2>0 && pOp->p2nOp ); + VdbeBranchTaken(res!=0,2); + if( res ) goto jump_to_p2; + break; + } +@@ -95946,10 +95969,11 @@ + pC->cacheStatus = CACHE_STALE; + VdbeBranchTaken(rc==SQLITE_OK,2); + if( rc==SQLITE_OK ){ + pC->nullRow = 0; + p->aCounter[pOp->p5]++; ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_READ - LIBSQL_STMTSTATUS_BASE]++; + #ifdef SQLITE_TEST + sqlite3_search_count++; + #endif + goto jump_to_p2_and_check_for_interrupt; + } +@@ -95997,10 +96021,11 @@ + assert( pC!=0 ); + assert( !isSorter(pC) ); + pIn2 = &aMem[pOp->p2]; + assert( (pIn2->flags & MEM_Blob) || (pOp->p5 & OPFLAG_PREFORMAT) ); + if( pOp->p5 & OPFLAG_NCHANGE ) p->nChange++; ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_WRITTEN - LIBSQL_STMTSTATUS_BASE]++; + assert( pC->eCurType==CURTYPE_BTREE ); + assert( pC->isTable==0 ); + rc = ExpandBlob(pIn2); + if( rc ) goto abort_due_to_error; + x.nKey = pIn2->n; +@@ -96397,10 +96422,11 @@ + assert( p->readOnly==0 ); + assert( DbMaskTest(p->btreeMask, pOp->p2) ); + rc = sqlite3BtreeClearTable(db->aDb[pOp->p2].pBt, (u32)pOp->p1, &nChange); + if( pOp->p3 ){ + p->nChange += nChange; ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_WRITTEN - LIBSQL_STMTSTATUS_BASE] += nChange; + if( pOp->p3>0 ){ + assert( memIsValid(&aMem[pOp->p3]) ); + memAboutToChange(p, &aMem[pOp->p3]); + aMem[pOp->p3].u.i += nChange; + } +@@ -97879,10 +97905,11 @@ + ** some other method is next invoked on the save virtual table cursor. + */ + rc = pModule->xNext(pCur->uc.pVCur); + sqlite3VtabImportErrmsg(p, pVtab); + if( rc ) goto abort_due_to_error; ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_READ - LIBSQL_STMTSTATUS_BASE]++; + res = pModule->xEof(pCur->uc.pVCur); + VdbeBranchTaken(!res,2); + if( !res ){ + /* If there is data, jump to P2 */ + goto jump_to_p2_and_check_for_interrupt; +@@ -98000,10 +98027,11 @@ + rc = SQLITE_OK; + }else{ + p->errorAction = ((pOp->p5==OE_Replace) ? OE_Abort : pOp->p5); + } + }else{ ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_WRITTEN - LIBSQL_STMTSTATUS_BASE]++; + p->nChange++; + } + if( rc ) goto abort_due_to_error; + } + break; +diff -u5 -r sqlite-amalgamation-pristine/sqlite3.h sqlite-amalgamation-modified/sqlite3.h +--- sqlite-amalgamation-pristine/sqlite3.h 2022-12-28 06:26:39.000000000 -0800 ++++ sqlite-amalgamation-modified/sqlite3.h 2023-08-04 14:53:19.667535836 -0700 +@@ -146,11 +146,11 @@ + ** [sqlite3_libversion_number()], [sqlite3_sourceid()], + ** [sqlite_version()] and [sqlite_source_id()]. + */ + #define SQLITE_VERSION "3.40.1" + #define SQLITE_VERSION_NUMBER 3040001 +-#define SQLITE_SOURCE_ID "2022-12-28 14:03:47 df5c253c0b3dd24916e4ec7cf77d3db5294cc9fd45ae7b9c5e82ad8197f38a24" ++#define SQLITE_SOURCE_ID "2022-12-28 14:03:47 df5c253c0b3dd24916e4ec7cf77d3db5294cc9fd45ae7b9c5e82ad8197f3alt1" + + /* + ** CAPI3REF: Run-Time Library Version Numbers + ** KEYWORDS: sqlite3_version sqlite3_sourceid + ** +@@ -8615,10 +8615,18 @@ + ** used to store the prepared statement. ^This value is not actually + ** a counter, and so the resetFlg parameter to sqlite3_stmt_status() + ** is ignored when the opcode is SQLITE_STMTSTATUS_MEMUSED. + **
+ ** ++** ++** [[LIBSQL_STMTSTATUS_ROWS_READ]] ++** [[LIBSQL_STMTSTATUS_ROWS_WRITTEN]] ++**
LIBSQL_STMTSTATUS_ROWS_READ
++** LIBSQL_STMTSTATUS_ROWS_WRITTEN
++**
^LIBSQL_STMTSTATUS_ROWS_READ is the number of rows read when executing ++** this statement. LIBSQL_STMTSTATUS_ROWS_WRITTEN value is the number of ++** rows written. + */ + #define SQLITE_STMTSTATUS_FULLSCAN_STEP 1 + #define SQLITE_STMTSTATUS_SORT 2 + #define SQLITE_STMTSTATUS_AUTOINDEX 3 + #define SQLITE_STMTSTATUS_VM_STEP 4 +@@ -8626,10 +8634,14 @@ + #define SQLITE_STMTSTATUS_RUN 6 + #define SQLITE_STMTSTATUS_FILTER_MISS 7 + #define SQLITE_STMTSTATUS_FILTER_HIT 8 + #define SQLITE_STMTSTATUS_MEMUSED 99 + ++#define LIBSQL_STMTSTATUS_BASE 1024 ++#define LIBSQL_STMTSTATUS_ROWS_READ LIBSQL_STMTSTATUS_BASE + 1 ++#define LIBSQL_STMTSTATUS_ROWS_WRITTEN LIBSQL_STMTSTATUS_BASE + 2 ++ + /* + ** CAPI3REF: Custom Page Cache Object + ** + ** The sqlite3_pcache type is opaque. It is implemented by + ** the pluggable module. The SQLite core has no knowledge of diff --git a/patches/sqlite/0001-row-counts-plain.patch b/patches/sqlite/0001-row-counts-plain.patch new file mode 100644 index 00000000000..e814209692a --- /dev/null +++ b/patches/sqlite/0001-row-counts-plain.patch @@ -0,0 +1,251 @@ +# This patch contains code from libsql, which is released under the MIT license. +# +# See https://github.com/libsql/libsql for details. +diff -u5 -r sqlite-src-3400100-pristine/src/shell.c.in sqlite-src-3400100-modified/src/shell.c.in +--- sqlite-src-3400100-pristine/src/shell.c.in 2023-08-03 14:15:24.760062869 -0700 ++++ sqlite-src-3400100-modified/src/shell.c.in 2023-08-03 14:15:53.042646929 -0700 +@@ -2952,10 +2952,15 @@ + raw_printf(pArg->out, "Reprepare operations: %d\n", iCur); + iCur = sqlite3_stmt_status(pArg->pStmt, SQLITE_STMTSTATUS_RUN, bReset); + raw_printf(pArg->out, "Number of times run: %d\n", iCur); + iCur = sqlite3_stmt_status(pArg->pStmt, SQLITE_STMTSTATUS_MEMUSED, bReset); + raw_printf(pArg->out, "Memory used by prepared stmt: %d\n", iCur); ++ ++ iCur = sqlite3_stmt_status(pArg->pStmt, LIBSQL_STMTSTATUS_ROWS_READ, bReset); ++ raw_printf(pArg->out, "Rows read: %d\n", iCur); ++ iCur = sqlite3_stmt_status(pArg->pStmt, LIBSQL_STMTSTATUS_ROWS_WRITTEN, bReset); ++ raw_printf(pArg->out, "Rows written: %d\n", iCur); + } + + #ifdef __linux__ + displayLinuxIoStats(pArg->out); + #endif +diff -u5 -r sqlite-src-3400100-pristine/src/sqlite.h.in sqlite-src-3400100-modified/src/sqlite.h.in +--- sqlite-src-3400100-pristine/src/sqlite.h.in 2022-12-28 06:26:33.000000000 -0800 ++++ sqlite-src-3400100-modified/src/sqlite.h.in 2023-08-03 14:18:49.490588655 -0700 +@@ -8615,10 +8615,18 @@ + ** used to store the prepared statement. ^This value is not actually + ** a counter, and so the resetFlg parameter to sqlite3_stmt_status() + ** is ignored when the opcode is SQLITE_STMTSTATUS_MEMUSED. + **
+ ** ++** ++** [[LIBSQL_STMTSTATUS_ROWS_READ]] ++** [[LIBSQL_STMTSTATUS_ROWS_WRITTEN]] ++**
LIBSQL_STMTSTATUS_ROWS_READ
++** LIBSQL_STMTSTATUS_ROWS_WRITTEN
++**
^LIBSQL_STMTSTATUS_ROWS_READ is the number of rows read when executing ++** this statement. LIBSQL_STMTSTATUS_ROWS_WRITTEN value is the number of ++** rows written. + */ + #define SQLITE_STMTSTATUS_FULLSCAN_STEP 1 + #define SQLITE_STMTSTATUS_SORT 2 + #define SQLITE_STMTSTATUS_AUTOINDEX 3 + #define SQLITE_STMTSTATUS_VM_STEP 4 +@@ -8626,10 +8634,14 @@ + #define SQLITE_STMTSTATUS_RUN 6 + #define SQLITE_STMTSTATUS_FILTER_MISS 7 + #define SQLITE_STMTSTATUS_FILTER_HIT 8 + #define SQLITE_STMTSTATUS_MEMUSED 99 + ++#define LIBSQL_STMTSTATUS_BASE 1024 ++#define LIBSQL_STMTSTATUS_ROWS_READ LIBSQL_STMTSTATUS_BASE + 1 ++#define LIBSQL_STMTSTATUS_ROWS_WRITTEN LIBSQL_STMTSTATUS_BASE + 2 ++ + /* + ** CAPI3REF: Custom Page Cache Object + ** + ** The sqlite3_pcache type is opaque. It is implemented by + ** the pluggable module. The SQLite core has no knowledge of +diff -u5 -r sqlite-src-3400100-pristine/src/vdbeapi.c sqlite-src-3400100-modified/src/vdbeapi.c +--- sqlite-src-3400100-pristine/src/vdbeapi.c 2022-12-28 06:26:33.000000000 -0800 ++++ sqlite-src-3400100-modified/src/vdbeapi.c 2023-08-03 14:19:40.916453104 -0700 +@@ -1826,11 +1826,11 @@ + int sqlite3_stmt_status(sqlite3_stmt *pStmt, int op, int resetFlag){ + Vdbe *pVdbe = (Vdbe*)pStmt; + u32 v; + #ifdef SQLITE_ENABLE_API_ARMOR + if( !pStmt +- || (op!=SQLITE_STMTSTATUS_MEMUSED && (op<0||op>=ArraySize(pVdbe->aCounter))) ++ || (op!=SQLITE_STMTSTATUS_MEMUSED && (op<0||(op>=ArraySize(pVdbe->aCounter)&&oplookaside.pEnd = db->lookaside.pStart; + sqlite3VdbeDelete(pVdbe); + db->pnBytesFreed = 0; + db->lookaside.pEnd = db->lookaside.pTrueEnd; + sqlite3_mutex_leave(db->mutex); ++ }else if( op>=LIBSQL_STMTSTATUS_BASE ){ ++ v = pVdbe->aLibsqlCounter[op - LIBSQL_STMTSTATUS_BASE]; ++ if( resetFlag ) pVdbe->aLibsqlCounter[op - LIBSQL_STMTSTATUS_BASE] = 0; + }else{ + v = pVdbe->aCounter[op]; + if( resetFlag ) pVdbe->aCounter[op] = 0; + } + return (int)v; +diff -u5 -r sqlite-src-3400100-pristine/src/vdbe.c sqlite-src-3400100-modified/src/vdbe.c +--- sqlite-src-3400100-pristine/src/vdbe.c 2022-12-28 06:26:33.000000000 -0800 ++++ sqlite-src-3400100-modified/src/vdbe.c 2023-08-03 14:26:29.473777273 -0700 +@@ -3582,10 +3582,11 @@ + if( pOp->p3 ){ + nEntry = sqlite3BtreeRowCountEst(pCrsr); + }else{ + nEntry = 0; /* Not needed. Only used to silence a warning. */ + rc = sqlite3BtreeCount(db, pCrsr, &nEntry); ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_READ - LIBSQL_STMTSTATUS_BASE] += nEntry; + if( rc ) goto abort_due_to_error; + } + pOut = out2Prerelease(p, pOp); + pOut->u.i = nEntry; + goto check_for_interrupt; +@@ -4741,10 +4742,11 @@ + if( eqOnly && r.eqSeen==0 ){ + assert( res!=0 ); + goto seek_not_found; + } + } ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_READ - LIBSQL_STMTSTATUS_BASE]++; + #ifdef SQLITE_TEST + sqlite3_search_count++; + #endif + if( oc>=OP_SeekGE ){ assert( oc==OP_SeekGE || oc==OP_SeekGT ); + if( res<0 || (res==0 && oc==OP_SeekGT) ){ +@@ -5309,10 +5311,11 @@ + pC->nullRow = 0; + pC->cacheStatus = CACHE_STALE; + pC->deferredMoveto = 0; + VdbeBranchTaken(res!=0,2); + pC->seekResult = res; ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_READ - LIBSQL_STMTSTATUS_BASE]++; + if( res!=0 ){ + assert( rc==SQLITE_OK ); + if( pOp->p2==0 ){ + rc = SQLITE_CORRUPT_BKPT; + }else{ +@@ -5565,10 +5568,11 @@ + } + } + if( pOp->p5 & OPFLAG_ISNOOP ) break; + #endif + ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_WRITTEN - LIBSQL_STMTSTATUS_BASE]++; + if( pOp->p5 & OPFLAG_NCHANGE ) p->nChange++; + if( pOp->p5 & OPFLAG_LASTROWID ) db->lastRowid = x.nKey; + assert( (pData->flags & (MEM_Blob|MEM_Str))!=0 || pData->n==0 ); + x.pData = pData->z; + x.nData = pData->n; +@@ -5748,10 +5752,11 @@ + pC->seekResult = 0; + if( rc ) goto abort_due_to_error; + + /* Invoke the update-hook if required. */ + if( opflags & OPFLAG_NCHANGE ){ ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_WRITTEN - LIBSQL_STMTSTATUS_BASE]++; + p->nChange++; + if( db->xUpdateCallback && ALWAYS(pTab!=0) && HasRowid(pTab) ){ + db->xUpdateCallback(db->pUpdateArg, SQLITE_DELETE, zDb, pTab->zName, + pC->movetoTarget); + assert( pC->iDb>=0 ); +@@ -6035,10 +6040,11 @@ + rc = sqlite3BtreeLast(pCrsr, &res); + pC->nullRow = (u8)res; + pC->deferredMoveto = 0; + pC->cacheStatus = CACHE_STALE; + if( rc ) goto abort_due_to_error; ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_READ - LIBSQL_STMTSTATUS_BASE]++; + if( pOp->p2>0 ){ + VdbeBranchTaken(res!=0,2); + if( res ) goto jump_to_p2; + } + break; +@@ -6139,10 +6145,11 @@ + pC->deferredMoveto = 0; + pC->cacheStatus = CACHE_STALE; + } + if( rc ) goto abort_due_to_error; + pC->nullRow = (u8)res; ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_READ - LIBSQL_STMTSTATUS_BASE]++; + assert( pOp->p2>0 && pOp->p2nOp ); + VdbeBranchTaken(res!=0,2); + if( res ) goto jump_to_p2; + break; + } +@@ -6243,10 +6250,11 @@ + pC->cacheStatus = CACHE_STALE; + VdbeBranchTaken(rc==SQLITE_OK,2); + if( rc==SQLITE_OK ){ + pC->nullRow = 0; + p->aCounter[pOp->p5]++; ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_READ - LIBSQL_STMTSTATUS_BASE]++; + #ifdef SQLITE_TEST + sqlite3_search_count++; + #endif + goto jump_to_p2_and_check_for_interrupt; + } +@@ -6294,10 +6302,11 @@ + assert( pC!=0 ); + assert( !isSorter(pC) ); + pIn2 = &aMem[pOp->p2]; + assert( (pIn2->flags & MEM_Blob) || (pOp->p5 & OPFLAG_PREFORMAT) ); + if( pOp->p5 & OPFLAG_NCHANGE ) p->nChange++; ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_WRITTEN - LIBSQL_STMTSTATUS_BASE]++; + assert( pC->eCurType==CURTYPE_BTREE ); + assert( pC->isTable==0 ); + rc = ExpandBlob(pIn2); + if( rc ) goto abort_due_to_error; + x.nKey = pIn2->n; +@@ -6694,10 +6703,11 @@ + assert( p->readOnly==0 ); + assert( DbMaskTest(p->btreeMask, pOp->p2) ); + rc = sqlite3BtreeClearTable(db->aDb[pOp->p2].pBt, (u32)pOp->p1, &nChange); + if( pOp->p3 ){ + p->nChange += nChange; ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_WRITTEN - LIBSQL_STMTSTATUS_BASE] += nChange; + if( pOp->p3>0 ){ + assert( memIsValid(&aMem[pOp->p3]) ); + memAboutToChange(p, &aMem[pOp->p3]); + aMem[pOp->p3].u.i += nChange; + } +@@ -8176,10 +8186,11 @@ + ** some other method is next invoked on the save virtual table cursor. + */ + rc = pModule->xNext(pCur->uc.pVCur); + sqlite3VtabImportErrmsg(p, pVtab); + if( rc ) goto abort_due_to_error; ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_READ - LIBSQL_STMTSTATUS_BASE]++; + res = pModule->xEof(pCur->uc.pVCur); + VdbeBranchTaken(!res,2); + if( !res ){ + /* If there is data, jump to P2 */ + goto jump_to_p2_and_check_for_interrupt; +@@ -8297,10 +8308,11 @@ + rc = SQLITE_OK; + }else{ + p->errorAction = ((pOp->p5==OE_Replace) ? OE_Abort : pOp->p5); + } + }else{ ++ p->aLibsqlCounter[LIBSQL_STMTSTATUS_ROWS_WRITTEN - LIBSQL_STMTSTATUS_BASE]++; + p->nChange++; + } + if( rc ) goto abort_due_to_error; + } + break; +diff -u5 -r sqlite-src-3400100-pristine/src/vdbeInt.h sqlite-src-3400100-modified/src/vdbeInt.h +--- sqlite-src-3400100-pristine/src/vdbeInt.h 2022-12-28 06:26:33.000000000 -0800 ++++ sqlite-src-3400100-modified/src/vdbeInt.h 2023-08-03 14:26:54.088987824 -0700 +@@ -468,10 +468,11 @@ + bft readOnly:1; /* True for statements that do not write */ + bft bIsReader:1; /* True for statements that read */ + yDbMask btreeMask; /* Bitmask of db->aDb[] entries referenced */ + yDbMask lockMask; /* Subset of btreeMask that requires a lock */ + u32 aCounter[9]; /* Counters used by sqlite3_stmt_status() */ ++ u32 aLibsqlCounter[3]; /* libSQL extension: Counters used by sqlite3_stmt_status()*/ + char *zSql; /* Text of the SQL statement that generated this */ + #ifdef SQLITE_ENABLE_NORMALIZE + char *zNormSql; /* Normalization of the associated SQL statement */ + DblquoteStr *pDblStr; /* List of double-quoted string literals */ + #endif diff --git a/patches/sqlite/README b/patches/sqlite/README new file mode 100644 index 00000000000..23959a004db --- /dev/null +++ b/patches/sqlite/README @@ -0,0 +1,69 @@ +The patches for SQLite are a bit weird due to SQLite's idiosyncratic +build process. + +SQLite comes in two forms. First, there's the "plain" form. It looks a +lot like a typical open-source C project: there's a bunch of .c and .h +files, a Makefile, a configure script, and various other files. + +Second, there's the "amalgamation". This is all of SQLite combined +into two .c and two .h files. workerd consumes this form. + +workerd has to patch SQLite, hence the patches in this directory. +Patching the amalgamation is painful. The vast majority of the code is +in a single 250,000-line .c file. Some pieces of code are duplicated +in the building of the amalgamation, so both spots need patching. +SQLite's tests are not included in the amalgamation, so testing an +amalgamation patch is hard. + +Making workerd consume the plain form would also be painful. To build +the amalgamation, SQLite uses a parser generator, a handful of +utilities written in C, and a whole lot of tclsh scripts all invoked +by a Makefile. To build all this from Bazel, we would need to either +rewrite tens of thousands of lines of TCL or include TCL as a build +dependency of workerd. + +Instead, we have this compromise solution. Each patch in here is +duplicated: one copy for the plain form and one for the amalgamation. +Bazel downloads the SQLite amalgamation and patches it. + +To update to a new SQLite version, obtain the new SQLite in both plain +and amalgamated forms. Apply the plain-form patch to SQLite, fixing as +necessary, and replace the plain-form patch with the fixed version. + +Then, use the patched SQLite to build an amalgamation. Diff that +against the downloaded amalgamation to produce a new amalgamated-form +patch, and replace the existing amalgamated patch with the new one. + + +Example, assuming the new SQLite has been downloaded into the current +directory: + +$ unzip sqlite-src-$VERSION.zip +$ mv sqlite-src-$VERSION sqlite-src-pristine +$ unzip sqlite-src-$VERSION.zip # yes, again +$ mv sqlite-src-$VERSION sqlite-src-modified +$ unzip sqlite-amalgamation-$VERSION.zip +$ mv sqlite-amalgamation-$VERSION.zip sqlite-amalgamation-pristine +$ mkdir sqlite-amalgamation-modified + +Now patch: + +$ cd sqlite-src-modified +$ patch -p1 < /path/to/workerd/patches/sqlite/0001-row-counts-plain.patch +$ ./configure && make test + +Make sure the tests pass. If the patch needed any modification, regenerate it: + +$ diff -u5 -r sqlite-src-pristine sqlite-src-modified \ + | grep -v "Only in sqlite-src-modified" \ + > /path/to/workerd/patches/sqlite/0001-row-counts-plain.patch + +Build the amalagamation and the patch: + +$ make sqlite3.c sqlite3.h sqlite3ext.h shell.c +$ cp shell.c sqlite3.c sqlite3.h sqlite3ext.h ../sqlite-amalgamation-modified/ +$ cd .. +$ diff -u5 -r sqlite-amalgamation-pristine sqlite-amalgamation-modified \ + > /path/to/workerd/patches/sqlite/0001-row-counts-amalgamation.patch + +Repeat for each patch. diff --git a/src/workerd/api/sql-test.js b/src/workerd/api/sql-test.js index f2db9a3fba1..c112e319c81 100644 --- a/src/workerd/api/sql-test.js +++ b/src/workerd/api/sql-test.js @@ -670,6 +670,64 @@ async function test(storage) { ) } +async function testIoStats(storage) { + const sql = storage.sql; + + sql.exec(`CREATE TABLE tbl (id INTEGER PRIMARY KEY, value TEXT)`); + sql.exec(`INSERT INTO tbl (id, value) VALUES (?, ?)`, 100000, "arbitrary-initial-value"); + await scheduler.wait(1); + + // When writing, the rowsWritten count goes up. + { + const cursor = sql.exec(`INSERT INTO tbl (id, value) VALUES (?, ?)`, 1, "arbitrary-value"); + Array.from(cursor); // Consume all the results + assert.equal(cursor.rowsWritten, 1); + } + + // When reading, the rowsRead count goes up. + { + const cursor = sql.exec(`SELECT * FROM tbl`); + Array.from(cursor); // Consume all the results + assert.equal(cursor.rowsRead, 2); + } + + // Each invocation of a prepared statement gets its own counters. + { + const id1 = 101; + const id2 = 202; + + const prepared = sql.prepare(`INSERT INTO tbl (id, value) VALUES (?, ?)`); + const cursor123 = prepared(id1, "value1"); + Array.from(cursor123); + assert.equal(cursor123.rowsWritten, 1); + + const cursor456 = prepared(id2, "value2"); + Array.from(cursor456); + assert.equal(cursor456.rowsWritten, 1); + assert.equal(cursor123.rowsWritten, 1); // remained unchanged + } + + // Row counters are updated as you consume the cursor. + { + sql.exec(`DELETE FROM tbl`); + const prepared = sql.prepare(`INSERT INTO tbl (id, value) VALUES (?, ?)`); + for (let i = 1; i <= 10; i++) { + Array.from(prepared(i, "value" + i)); + } + + const cursor = sql.exec(`SELECT * FROM tbl`); + const resultsIterator = cursor[Symbol.iterator](); + let rowsSeen = 0; + while (true) { + const result = resultsIterator.next(); + if (result.done) { + break; + } + assert.equal(++rowsSeen, cursor.rowsRead); + } + } +} + async function testForeignKeys(storage) { const sql = storage.sql @@ -763,6 +821,9 @@ export class DurableObjectExample { // abort() always throws. throw new Error("can't get here") + } else if (req.url.endsWith('/sql-test-io-stats')) { + await testIoStats(this.state.storage) + return Response.json({ ok: true }) } throw new Error('unknown url: ' + req.url) @@ -783,6 +844,9 @@ export default { // Test SQL API assert.deepEqual(await doReq('sql-test'), { ok: true }) + // Test SQL IO stats + assert.deepEqual(await doReq("sql-test-io-stats"), {ok: true}); + // Test defer_foreign_keys (explodes the DO) await assert.rejects(async () => { await doReq('sql-test-foreign-keys') diff --git a/src/workerd/api/sql.c++ b/src/workerd/api/sql.c++ index adbdb97fa30..db2659bdae9 100644 --- a/src/workerd/api/sql.c++ +++ b/src/workerd/api/sql.c++ @@ -91,6 +91,22 @@ void SqlStorage::Cursor::CachedColumnNames::ensureInitialized( } } +double SqlStorage::Cursor::getRowsRead() { + KJ_IF_MAYBE(st, state) { + return static_cast((**st).query.getRowsRead()); + } else { + return static_cast(rowsRead); + } +} + +double SqlStorage::Cursor::getRowsWritten() { + KJ_IF_MAYBE(st, state) { + return static_cast((**st).query.getRowsWritten()); + } else { + return static_cast(rowsWritten); + } +} + jsg::Ref SqlStorage::Cursor::rows(jsg::Lock& js) { KJ_IF_MAYBE(s, state) { cachedColumnNames.ensureInitialized(js, (*s)->query); @@ -168,7 +184,11 @@ auto SqlStorage::Cursor::iteratorImpl(jsg::Lock& js, jsg::Ref& obj, Func } auto& query = state.query; + if (query.isDone()) { + // Save off row counts before the query goes away. + obj->rowsRead = query.getRowsRead(); + obj->rowsWritten = query.getRowsWritten(); // Clean up the query proactively. obj->state = nullptr; return nullptr; diff --git a/src/workerd/api/sql.h b/src/workerd/api/sql.h index 3044e651fb1..4cce429e595 100644 --- a/src/workerd/api/sql.h +++ b/src/workerd/api/sql.h @@ -97,11 +97,16 @@ class SqlStorage::Cursor final: public jsg::Object { cachedColumnNames(cachedColumnNames) {} ~Cursor() noexcept(false); + double getRowsRead(); + double getRowsWritten(); + kj::Array> getColumnNames(jsg::Lock& js); JSG_RESOURCE_TYPE(Cursor, CompatibilityFlags::Reader flags) { JSG_ITERABLE(rows); JSG_METHOD(raw); JSG_READONLY_PROTOTYPE_PROPERTY(columnNames, getColumnNames); + JSG_READONLY_PROTOTYPE_PROPERTY(rowsRead, getRowsRead); + JSG_READONLY_PROTOTYPE_PROPERTY(rowsWritten, getRowsWritten); } @@ -164,6 +169,11 @@ class SqlStorage::Cursor final: public jsg::Object { kj::Maybe> statement; // If this cursor was created from a prepared statement, this keeps the statement object alive. + uint64_t rowsRead = 0; + uint64_t rowsWritten = 0; + // Row IO counts. These are updated as the query runs. We keep these outside the State so they + // remain available even after the query is done or canceled. + kj::Maybe ownCachedColumnNames; CachedColumnNames& cachedColumnNames; diff --git a/src/workerd/util/sqlite-test.c++ b/src/workerd/util/sqlite-test.c++ index 9ea2b6787bc..1a9709761ae 100644 --- a/src/workerd/util/sqlite-test.c++ +++ b/src/workerd/util/sqlite-test.c++ @@ -3,6 +3,7 @@ // https://opensource.org/licenses/Apache-2.0 #include "sqlite.h" +#include #include #include #include @@ -366,5 +367,256 @@ KJ_TEST("SQLite onWrite callback") { KJ_EXPECT(sawWrite); } +struct RowCounts { + uint64_t found; + uint64_t read; + uint64_t written; +}; + +template +RowCounts countRowsTouched(SqliteDatabase& db, SqliteDatabase::Regulator& regulator, kj::StringPtr sqlCode, Params... bindParams) { + uint64_t rowsFound = 0; + + // Runs a query; retrieves and discards all the data. + auto query = db.run(regulator, sqlCode, bindParams...); + while (!query.isDone()) { + rowsFound++; + query.nextRow(); + } + + return {.found = rowsFound, + .read = query.getRowsRead(), + .written = query.getRowsWritten()}; +} + +template +RowCounts countRowsTouched(SqliteDatabase& db, kj::StringPtr sqlCode, Params... bindParams) { + return countRowsTouched(db, SqliteDatabase::TRUSTED, sqlCode, std::forward(bindParams)...); +} + +KJ_TEST("SQLite read row counters (basic)") { + auto dir = kj::newInMemoryDirectory(kj::nullClock()); + SqliteDatabase::Vfs vfs(*dir); + SqliteDatabase db(vfs, kj::Path({"foo"}), kj::WriteMode::CREATE | kj::WriteMode::MODIFY); + + db.run(R"( + CREATE TABLE things ( + id INTEGER PRIMARY KEY, + unindexed_int INTEGER, + value TEXT + ); + )"); + + constexpr int dbRowCount = 1000; + auto insertStmt = db.prepare("INSERT INTO things (id, unindexed_int, value) VALUES (?, ?, ?)"); + for (int i = 0; i < dbRowCount; i++) { + insertStmt.run(i, i * 1000, kj::str("value", i)); + } + + // Sanity check that the inserts worked. + { + auto getCount = db.prepare("SELECT COUNT(*) FROM things"); + KJ_EXPECT(getCount.run().getInt(0) == dbRowCount); + } + + // Selecting all the rows reads all the rows. + { + RowCounts stats = countRowsTouched(db, "SELECT * FROM things"); + KJ_EXPECT(stats.found == dbRowCount); + KJ_EXPECT(stats.read == dbRowCount); + KJ_EXPECT(stats.written == 0); + } + + // Selecting one row using an index reads one row. + { + RowCounts stats = countRowsTouched(db, "SELECT * FROM things WHERE id=?", 5); + KJ_EXPECT(stats.found == 1); + KJ_EXPECT(stats.read == 1); + KJ_EXPECT(stats.written == 0); + } + + // Selecting one row using an reads one row, even if that row is in the middle of the table. + { + RowCounts stats = countRowsTouched(db, "SELECT * FROM things WHERE id=?", dbRowCount / 2); + KJ_EXPECT(stats.found == 1); + KJ_EXPECT(stats.read == 1); + KJ_EXPECT(stats.written == 0); + } + + // Selecting a row by an unindexed value reads the whole table. + { + RowCounts stats = countRowsTouched(db, "SELECT * FROM things WHERE unindexed_int = ?", 5000); + KJ_EXPECT(stats.found == 1); + KJ_EXPECT(stats.read == dbRowCount); + KJ_EXPECT(stats.written == 0); + } + + // Selecting an unindexed aggregate scans all the rows, which counts as reading them. + { + RowCounts stats = countRowsTouched(db, "SELECT MAX(unindexed_int) FROM things"); + KJ_EXPECT(stats.found == 1); + KJ_EXPECT(stats.read == dbRowCount); + KJ_EXPECT(stats.written == 0); + } + + // Selecting an indexed aggregate can use the index, so it only reads the row it found. + { + RowCounts stats = countRowsTouched(db, "SELECT MIN(id) FROM things"); + KJ_EXPECT(stats.found == 1); + KJ_EXPECT(stats.read == 1); + KJ_EXPECT(stats.written == 0); + } + + // Selecting with a limit only reads the returned rows. + { + RowCounts stats = countRowsTouched(db, "SELECT * FROM things LIMIT 5"); + KJ_EXPECT(stats.found == 5); + KJ_EXPECT(stats.read == 5); + KJ_EXPECT(stats.written == 0); + } +} + +KJ_TEST("SQLite write row counters (basic)") { + auto dir = kj::newInMemoryDirectory(kj::nullClock()); + SqliteDatabase::Vfs vfs(*dir); + SqliteDatabase db(vfs, kj::Path({"foo"}), kj::WriteMode::CREATE | kj::WriteMode::MODIFY); + + db.run(R"( + CREATE TABLE things ( + id INTEGER PRIMARY KEY + ); + )"); + + db.run(R"( + CREATE TABLE unindexed_things ( + id INTEGER + ); + )"); + + // Inserting a row counts as one row written. + { + RowCounts stats = countRowsTouched(db, "INSERT INTO unindexed_things (id) VALUES (?)", 1); + KJ_EXPECT(stats.read == 0); + KJ_EXPECT(stats.written == 1); + } + + // Inserting a row into a table with a primary key will also do a read (to ensure there's no + // duplicate PK). + { + RowCounts stats = countRowsTouched(db, "INSERT INTO things (id) VALUES (?)", 1); + KJ_EXPECT(stats.read == 1); + KJ_EXPECT(stats.written == 1); + } + + // Deleting a row counts as a write. + { + RowCounts stats = countRowsTouched(db, "INSERT INTO things (id) VALUES (?)", 123); + KJ_EXPECT(stats.written == 1); + + stats = countRowsTouched(db, "DELETE FROM things WHERE id=?", 123); + KJ_EXPECT(stats.read == 1); + KJ_EXPECT(stats.written == 1); + } + + // Deleting nothing is not a write. + { + RowCounts stats = countRowsTouched(db, "DELETE FROM things WHERE id=?", 998877112233); + KJ_EXPECT(stats.written == 0); + } + + // Inserting many things is many writes. + { + db.run("DELETE FROM things"); + db.run("INSERT INTO things (id) VALUES (1)"); + db.run("INSERT INTO things (id) VALUES (3)"); + db.run("INSERT INTO things (id) VALUES (5)"); + + RowCounts stats = countRowsTouched(db, "INSERT INTO unindexed_things (id) SELECT id FROM things"); + KJ_EXPECT(stats.read == 3); + KJ_EXPECT(stats.written == 3); + } + + // Each updated row is a write. + { + db.run("DELETE FROM unindexed_things"); + db.run("INSERT INTO unindexed_things (id) VALUES (1)"); + db.run("INSERT INTO unindexed_things (id) VALUES (2)"); + db.run("INSERT INTO unindexed_things (id) VALUES (3)"); + db.run("INSERT INTO unindexed_things (id) VALUES (4)"); + + RowCounts stats = countRowsTouched(db, "UPDATE unindexed_things SET id = id * 10 WHERE id >= 3"); + KJ_EXPECT(stats.written == 2); + } + + // On an indexed table, each updated row is two writes. This is probably due to the index update. + { + db.run("DELETE FROM things"); + db.run("INSERT INTO things (id) VALUES (1)"); + db.run("INSERT INTO things (id) VALUES (2)"); + db.run("INSERT INTO things (id) VALUES (3)"); + db.run("INSERT INTO things (id) VALUES (4)"); + + RowCounts stats = countRowsTouched(db, "UPDATE things SET id = id * 10 WHERE id >= 3"); + KJ_EXPECT(stats.read >= 4); // At least one read per updated row + KJ_EXPECT(stats.written == 4); + } +} + +KJ_TEST("SQLite row counters with triggers") { + auto dir = kj::newInMemoryDirectory(kj::nullClock()); + SqliteDatabase::Vfs vfs(*dir); + SqliteDatabase db(vfs, kj::Path({"foo"}), kj::WriteMode::CREATE | kj::WriteMode::MODIFY); + + class RegulatorImpl: public SqliteDatabase::Regulator { + public: + RegulatorImpl() = default; + + bool isAllowedTrigger(kj::StringPtr name) override { + // SqliteDatabase::TRUSTED doesn't let us use triggers at all. + return true; + } + }; + + RegulatorImpl regulator; + + db.run(R"( + CREATE TABLE things ( + id INTEGER PRIMARY KEY + ); + + CREATE TABLE log ( + id INTEGER, + verb TEXT + ); + + CREATE TRIGGER log_inserts AFTER INSERT ON things + BEGIN + insert into log (id, verb) VALUES (NEW.id, "INSERT"); + END; + + CREATE TRIGGER log_deletes AFTER DELETE ON things + BEGIN + insert into log (id, verb) VALUES (OLD.id, "DELETE"); + END; + )"); + + // Each insert incurs two writes: one for the row in `things` and one for the row in `log`. + { + RowCounts stats = countRowsTouched(db, regulator, "INSERT INTO things (id) VALUES (1)"); + KJ_EXPECT(stats.written == 2); + } + + // A deletion incurs two writes: one for the row and one for the log. + { + db.run(regulator, "DELETE FROM things"); + db.run(regulator, "INSERT INTO things (id) VALUES (1)"); + db.run(regulator, "INSERT INTO things (id) VALUES (2)"); + db.run(regulator, "INSERT INTO things (id) VALUES (3)"); + + RowCounts stats = countRowsTouched(db, regulator, "DELETE FROM things"); + KJ_EXPECT(stats.written == 6); + } +} + } // namespace } // namespace workerd diff --git a/src/workerd/util/sqlite.c++ b/src/workerd/util/sqlite.c++ index 8a1ba88faab..6e134367b84 100644 --- a/src/workerd/util/sqlite.c++ +++ b/src/workerd/util/sqlite.c++ @@ -760,6 +760,9 @@ SqliteDatabase::Statement SqliteDatabase::prepare(Regulator& regulator, kj::Stri SqliteDatabase::Query::Query(SqliteDatabase& db, Regulator& regulator, Statement& statement, kj::ArrayPtr bindings) : db(db), regulator(regulator), statement(statement) { + // If the statement was used for a previous query, then its row counters contain data from that + // query's execution. Reset them to zero. + resetRowCounters(); init(bindings); } @@ -828,6 +831,22 @@ void SqliteDatabase::Query::bind(uint i, ValuePtr value) { } } +uint64_t SqliteDatabase::Query::getRowsRead() { + KJ_REQUIRE(statement != nullptr); + return sqlite3_stmt_status(statement, LIBSQL_STMTSTATUS_ROWS_READ, 0); +} + +uint64_t SqliteDatabase::Query::getRowsWritten() { + KJ_REQUIRE(statement != nullptr); + return sqlite3_stmt_status(statement, LIBSQL_STMTSTATUS_ROWS_WRITTEN, 0); +} + +void SqliteDatabase::Query::resetRowCounters() { + KJ_REQUIRE(statement != nullptr); + sqlite3_stmt_status(statement, LIBSQL_STMTSTATUS_ROWS_READ, 1); + sqlite3_stmt_status(statement, LIBSQL_STMTSTATUS_ROWS_WRITTEN, 1); +} + void SqliteDatabase::Query::bind(uint i, kj::ArrayPtr value) { SQLITE_CALL(sqlite3_bind_blob(statement, i+1, value.begin(), value.size(), SQLITE_STATIC)); } diff --git a/src/workerd/util/sqlite.h b/src/workerd/util/sqlite.h index 58166bbd16f..ca7ca52a0d3 100644 --- a/src/workerd/util/sqlite.h +++ b/src/workerd/util/sqlite.h @@ -217,6 +217,10 @@ class SqliteDatabase::Query { ~Query() noexcept(false); KJ_DISALLOW_COPY_AND_MOVE(Query); + uint64_t getRowsRead(); + uint64_t getRowsWritten(); + // Row IO counters. + bool isDone() { return done; } // If true, there are no more rows. (When true, the methods below must not be called.) @@ -292,6 +296,7 @@ class SqliteDatabase::Query { void checkRequirements(size_t size); void init(kj::ArrayPtr bindings); + void resetRowCounters(); void bind(uint column, ValuePtr value); void bind(uint column, kj::ArrayPtr value);