From 30c2932b449e45640d9a07690cce092954fb72ff Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Tue, 2 Jun 2026 08:38:51 +0300 Subject: [PATCH 1/3] libsql-sqlite3: Make libsql_stmt_interrupt() abort an in-flight step libsql_stmt_interrupt() set a per-statement isInterrupted flag, but the VDBE execution loop only ever checked the connection-wide db->u1.isInterrupted. A statement already executing inside sqlite3_step() therefore ran to completion regardless of the request; the flag was only observed at the next step() entry. Check p->isInterrupted alongside db->u1.isInterrupted at both VDBE interrupt-check sites so an interrupt requested mid-execution aborts the running statement promptly with SQLITE_INTERRUPT, without touching the connection-wide interrupt state (other statements keep running). Set and clear the flag atomically, mirroring sqlite3_interrupt(), since the request may come from another thread. Tests: - test/interruptstmt.test: deterministic regression via a new sqlite_stmt_interrupt_count test hook, covering the in-loop check and the connection-flag-stays-clear property. - test/interrupttest.c: standalone multi-threaded test of the real cross-thread case (interrupt a step() in flight) and statement-level granularity. Build and run with `make interrupttest`. --- libsql-sqlite3/Makefile.in | 5 + libsql-sqlite3/main.mk | 5 + libsql-sqlite3/src/test1.c | 5 +- libsql-sqlite3/src/vdbe.c | 22 ++- libsql-sqlite3/src/vdbeapi.c | 6 +- libsql-sqlite3/src/vdbeaux.c | 2 +- libsql-sqlite3/test/interruptstmt.test | 109 ++++++++++++++ libsql-sqlite3/test/interrupttest.c | 190 +++++++++++++++++++++++++ 8 files changed, 337 insertions(+), 7 deletions(-) create mode 100644 libsql-sqlite3/test/interruptstmt.test create mode 100644 libsql-sqlite3/test/interrupttest.c diff --git a/libsql-sqlite3/Makefile.in b/libsql-sqlite3/Makefile.in index d46a7eca9c..277106991b 100644 --- a/libsql-sqlite3/Makefile.in +++ b/libsql-sqlite3/Makefile.in @@ -1682,6 +1682,11 @@ threadtest: threadtest3$(TEXE) threadtest5: sqlite3.c $(TOP)/test/threadtest5.c $(LTLINK) $(TOP)/test/threadtest5.c sqlite3.c -o $@ $(TLIBS) +# Multi-threaded test of the libsql_stmt_interrupt() API. Builds a small +# standalone binary; run it directly to check the result. +interrupttest: sqlite3.c $(TOP)/test/interrupttest.c + $(LTLINK) $(TOP)/test/interrupttest.c sqlite3.c -o $@ $(TLIBS) + # Standard install and cleanup targets # liblibsql_wasm_install: liblibsql_wasm diff --git a/libsql-sqlite3/main.mk b/libsql-sqlite3/main.mk index 9a3d3eb52c..5ebe32ec89 100644 --- a/libsql-sqlite3/main.mk +++ b/libsql-sqlite3/main.mk @@ -1047,6 +1047,11 @@ threadtest3$(EXE): sqlite3.o $(THREADTEST3_SRC) $(TOP)/src/test_multiplex.c threadtest: threadtest3$(EXE) ./threadtest3$(EXE) +# Multi-threaded test of the libsql_stmt_interrupt() API. Builds a small +# standalone binary; run it directly to check the result. +interrupttest$(EXE): sqlite3.o $(TOP)/test/interrupttest.c + $(TCCX) $(TOP)/test/interrupttest.c sqlite3.o -o $@ $(THREADLIB) + TEST_EXTENSION = $(SHPREFIX)testloadext.$(SO) $(TEST_EXTENSION): $(TOP)/src/test_loadext.c $(MKSHLIB) $(TOP)/src/test_loadext.c -o $(TEST_EXTENSION) diff --git a/libsql-sqlite3/src/test1.c b/libsql-sqlite3/src/test1.c index 5af066c6b2..d7645614ba 100644 --- a/libsql-sqlite3/src/test1.c +++ b/libsql-sqlite3/src/test1.c @@ -8970,6 +8970,7 @@ int Sqlitetest1_Init(Tcl_Interp *interp){ extern int sqlite3_search_count; extern int sqlite3_found_count; extern int sqlite3_interrupt_count; + extern int sqlite3_stmt_interrupt_count; extern int sqlite3_open_file_count; extern int sqlite3_sort_count; extern int sqlite3_current_time; @@ -9306,8 +9307,10 @@ int Sqlitetest1_Init(Tcl_Interp *interp){ (char*)&sqlite3_max_blobsize, TCL_LINK_INT); Tcl_LinkVar(interp, "sqlite_like_count", (char*)&sqlite3_like_count, TCL_LINK_INT); - Tcl_LinkVar(interp, "sqlite_interrupt_count", + Tcl_LinkVar(interp, "sqlite_interrupt_count", (char*)&sqlite3_interrupt_count, TCL_LINK_INT); + Tcl_LinkVar(interp, "sqlite_stmt_interrupt_count", + (char*)&sqlite3_stmt_interrupt_count, TCL_LINK_INT); Tcl_LinkVar(interp, "sqlite_open_file_count", (char*)&sqlite3_open_file_count, TCL_LINK_INT); Tcl_LinkVar(interp, "sqlite_current_time", diff --git a/libsql-sqlite3/src/vdbe.c b/libsql-sqlite3/src/vdbe.c index 5a01485b0b..ed69893867 100644 --- a/libsql-sqlite3/src/vdbe.c +++ b/libsql-sqlite3/src/vdbe.c @@ -72,6 +72,12 @@ int sqlite3_search_count = 0; */ #ifdef SQLITE_TEST int sqlite3_interrupt_count = 0; +/* +** As above, but simulates a statement-level interrupt +** (libsql_stmt_interrupt()) on the statement that is currently executing, +** rather than a connection-wide sqlite3_interrupt(). +*/ +int sqlite3_stmt_interrupt_count = 0; #endif /* @@ -895,7 +901,9 @@ int sqlite3VdbeExec( p->iCurrentTime = 0; assert( p->explain==0 ); db->busyHandler.nBusy = 0; - if( AtomicLoad(&db->u1.isInterrupted) ) goto abort_due_to_interrupt; + if( AtomicLoad(&db->u1.isInterrupted) || AtomicLoad(&p->isInterrupted) ){ + goto abort_due_to_interrupt; + } sqlite3VdbeIOTraceSql(p); #ifdef SQLITE_DEBUG sqlite3BeginBenignMalloc(); @@ -964,6 +972,12 @@ int sqlite3VdbeExec( sqlite3_interrupt(db); } } + if( sqlite3_stmt_interrupt_count>0 ){ + sqlite3_stmt_interrupt_count--; + if( sqlite3_stmt_interrupt_count==0 ){ + libsql_stmt_interrupt((sqlite3_stmt*)p); + } + } #endif /* Sanity checking on other operands */ @@ -1085,7 +1099,9 @@ case OP_Goto: { /* jump */ ** checks on every opcode. This helps sqlite3_step() to run about 1.5% ** faster according to "valgrind --tool=cachegrind" */ check_for_interrupt: - if( AtomicLoad(&db->u1.isInterrupted) ) goto abort_due_to_interrupt; + if( AtomicLoad(&db->u1.isInterrupted) || AtomicLoad(&p->isInterrupted) ){ + goto abort_due_to_interrupt; + } #ifndef SQLITE_OMIT_PROGRESS_CALLBACK /* Call the progress callback if it is configured and the required number ** of VDBE ops have been executed (either since this invocation of @@ -9430,7 +9446,7 @@ default: { /* This is really OP_Noop, OP_Explain */ ** flag. */ abort_due_to_interrupt: - assert( AtomicLoad(&db->u1.isInterrupted) ); + assert( AtomicLoad(&db->u1.isInterrupted) || AtomicLoad(&p->isInterrupted) ); rc = SQLITE_INTERRUPT; goto abort_due_to_error; } diff --git a/libsql-sqlite3/src/vdbeapi.c b/libsql-sqlite3/src/vdbeapi.c index 07c78fdadb..3568ed193a 100644 --- a/libsql-sqlite3/src/vdbeapi.c +++ b/libsql-sqlite3/src/vdbeapi.c @@ -897,7 +897,9 @@ void libsql_stmt_interrupt(sqlite3_stmt *pStmt){ (void)SQLITE_MISUSE_BKPT; return; } - v->isInterrupted = 1; + /* Set atomically: this may be called from a different thread than the one + ** executing the statement, mirroring sqlite3_interrupt(). */ + AtomicStore(&v->isInterrupted, 1); } /* @@ -915,7 +917,7 @@ int sqlite3_step(sqlite3_stmt *pStmt){ return SQLITE_MISUSE_BKPT; } db = v->db; - if( v->isInterrupted ){ + if( AtomicLoad(&v->isInterrupted) ){ rc = SQLITE_INTERRUPT; v->rc = rc; db->errCode = rc; diff --git a/libsql-sqlite3/src/vdbeaux.c b/libsql-sqlite3/src/vdbeaux.c index 8e540d6541..a6fee8ecce 100644 --- a/libsql-sqlite3/src/vdbeaux.c +++ b/libsql-sqlite3/src/vdbeaux.c @@ -3632,7 +3632,7 @@ int sqlite3VdbeReset(Vdbe *p){ #ifdef SQLITE_DEBUG p->nWrite = 0; #endif - p->isInterrupted = 0; + AtomicStore(&p->isInterrupted, 0); /* Save profiling information from this VDBE run. */ diff --git a/libsql-sqlite3/test/interruptstmt.test b/libsql-sqlite3/test/interruptstmt.test new file mode 100644 index 0000000000..46bf8b37b1 --- /dev/null +++ b/libsql-sqlite3/test/interruptstmt.test @@ -0,0 +1,109 @@ +# 2026 libsql +# +# 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 the libsql_stmt_interrupt() +# API, which interrupts a single prepared statement rather than every +# statement on the connection the way sqlite3_interrupt() does. +# +# The interesting property is that an interrupt requested while a statement +# is *executing* (inside sqlite3_step()) is observed from within the VDBE +# loop, not merely at the next entry to sqlite3_step(). Like interrupt.test, +# this is exercised deterministically via the ::sqlite_stmt_interrupt_count +# test variable: when positive it is decremented once per VDBE opcode and, +# on reaching zero, fires libsql_stmt_interrupt() on the running statement. + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +set DB [sqlite3_connection_pointer db] + +# A table large enough that a scan/join loops through the VDBE's +# check_for_interrupt label far more than 100 times, so that the small +# trigger values below all land mid-execution. (Note: the queries below scan +# the rows -- e.g. sum(a) -- rather than bare count(*), which the planner +# satisfies with a single OP_Count opcode and never loops.) +do_test interruptstmt-1.0 { + execsql { + CREATE TABLE t1(a); + WITH RECURSIVE c(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM c WHERE i<400) + INSERT INTO t1 SELECT i FROM c; + SELECT count(*) FROM t1; + } +} {400} + +# Run $sql with the statement-interrupt trigger armed to fire after $n VDBE +# opcodes. Returns a list of {errorcode catchsql-result}. +proc run_with_trigger {sql n} { + set ::sqlite_stmt_interrupt_count $n + set r [catchsql $sql] + set ::sqlite_stmt_interrupt_count 0 + list [db errorcode] $r +} + +# For a statement that runs many opcodes, arming the trigger at a low count +# must abort it mid-execution with SQLITE_INTERRUPT -- and must never set the +# connection-wide interrupt flag (that is the whole point of doing this at the +# statement level rather than with sqlite3_interrupt()). +foreach {tn sql} { + 1 {SELECT sum(a) FROM t1} + 2 {SELECT count(*) FROM t1 a, t1 b} +} { + foreach n {1 2 5 20 100} { + do_test interruptstmt-2.$tn.$n.rc { + run_with_trigger $sql $n + } {9 {1 interrupted}} + do_test interruptstmt-2.$tn.$n.flag { + sqlite3_is_interrupted $DB + } 0 + } +} + +# With the trigger set higher than the whole program, the statement runs to +# completion and returns the correct result. +do_test interruptstmt-3.1 { + run_with_trigger {SELECT sum(a) FROM t1} 1000000 +} {0 {0 80200}} +do_test interruptstmt-3.2 { + run_with_trigger {SELECT count(*) FROM t1 a, t1 b} 100000000 +} {0 {0 160000}} + +# The checks above confirm the statement ends in SQLITE_INTERRUPT, but a +# trailing sqlite3_step() would report that at step entry even if the running +# step had been allowed to finish first. To prove the interrupt is honored +# *mid-execution* (the in-loop check, not just the step-entry check), count how +# many rows the scan actually visited via a Tcl function and confirm it stopped +# well short of the full table. +set ::nrows 0 +proc rowtick {} { incr ::nrows; return 1 } +db function rowtick -argcount 0 rowtick + +do_test interruptstmt-5.1 { + set ::nrows 0 + set r [run_with_trigger {SELECT count(*) FROM t1 WHERE rowtick()} 50] + list $r [expr {$::nrows>0 && $::nrows<400}] +} {{9 {1 interrupted}} 1} + +# Sanity check the instrument: without an interrupt the same scan visits every +# row. +do_test interruptstmt-5.2 { + set ::nrows 0 + set r [run_with_trigger {SELECT count(*) FROM t1 WHERE rowtick()} 100000000] + list $r $::nrows +} {{0 {0 400}} 400} + +# After all of the above the connection must be left un-interrupted, and a +# fresh statement runs normally. +do_test interruptstmt-4.1 { + sqlite3_is_interrupted $DB +} 0 +do_test interruptstmt-4.2 { + execsql {SELECT count(*) FROM t1} +} {400} + +finish_test diff --git a/libsql-sqlite3/test/interrupttest.c b/libsql-sqlite3/test/interrupttest.c new file mode 100644 index 0000000000..f4cb766bc4 --- /dev/null +++ b/libsql-sqlite3/test/interrupttest.c @@ -0,0 +1,190 @@ +/* +** +** A multi-threaded test for libsql_stmt_interrupt(). +** +** Unlike sqlite3_interrupt(), which aborts every statement running on a +** connection, libsql_stmt_interrupt() targets a single prepared statement. +** The interesting (and historically broken) case is interrupting a statement +** that is *already executing* inside sqlite3_step() on another thread: the +** per-statement flag must be observed from within the VDBE execution loop, +** not just at the entry to sqlite3_step(). +** +** This is a standalone program (it has its own main()); it is not part of the +** Tcl testfixture. The deterministic, single-threaded regression test lives +** in test/interruptstmt.test; this binary provides the genuine cross-thread +** coverage. Build and run it from the libsql-sqlite3 directory with: +** +** make interrupttest && ./interrupttest +** +** (the Makefile target compiles this file against the freshly built +** amalgamation). Exit status is 0 if every check passes, non-zero otherwise. +*/ +#include "sqlite3.h" + +#include +#include +#include +#include +#include +#include + +/* Number of rows in the work table. A 2-way self join over this many rows is +** a single, long-running sqlite3_step() that loops OP_Next (and therefore +** passes through the VDBE's check_for_interrupt label) many times. Large +** enough that an un-interrupted run takes several seconds, so a prompt return +** unambiguously means the interrupt landed mid-step. */ +#define WORK_ROWS 40000 + +/* How long the test waits before triggering the interrupt, and the upper +** bound on how long the step is allowed to keep running afterwards. */ +#define DELAY_MS 250 +#define REACT_MS 2000 + +static int nFail = 0; + +#define CHECK(cond, msg) do{ \ + if( (cond) ){ \ + printf(" ok - %s\n", (msg)); \ + }else{ \ + printf(" FAIL - %s\n", (msg)); \ + nFail++; \ + } \ + }while(0) + +static double now_ms(void){ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec*1000.0 + ts.tv_nsec/1.0e6; +} + +static void must_ok(sqlite3 *db, int rc, const char *what){ + if( rc!=SQLITE_OK && rc!=SQLITE_DONE && rc!=SQLITE_ROW ){ + fprintf(stderr, "fatal: %s: %s\n", what, sqlite3_errmsg(db)); + exit(2); + } +} + +static void exec(sqlite3 *db, const char *sql){ + char *zErr = 0; + int rc = sqlite3_exec(db, sql, 0, 0, &zErr); + if( rc!=SQLITE_OK ){ + fprintf(stderr, "fatal: exec(%s): %s\n", sql, zErr ? zErr : "?"); + exit(2); + } +} + +/* Worker thread: drive a prepared statement to completion (or until it is +** interrupted), recording the final return code and how long it ran. */ +typedef struct Worker { + sqlite3_stmt *stmt; + int rc; + double elapsed_ms; +} Worker; + +static void *worker_step(void *arg){ + Worker *w = (Worker*)arg; + double t0 = now_ms(); + int rc; + do{ + rc = sqlite3_step(w->stmt); + }while( rc==SQLITE_ROW ); + w->rc = rc; + w->elapsed_ms = now_ms() - t0; + return 0; +} + +/* Run `stmt` on a background thread, wait DELAY_MS, then interrupt it via the +** supplied interrupt routine. Returns the worker result by value. */ +static Worker run_and_interrupt(sqlite3_stmt *stmt, + void (*interrupt)(void*), void *arg){ + Worker w; + pthread_t th; + memset(&w, 0, sizeof(w)); + w.stmt = stmt; + pthread_create(&th, 0, worker_step, &w); + usleep(DELAY_MS*1000); /* let the worker get well inside sqlite3_step() */ + interrupt(arg); + pthread_join(th, 0); + return w; +} + +static void stmt_interrupt_cb(void *arg){ + libsql_stmt_interrupt((sqlite3_stmt*)arg); +} + +/* +** Test 1: interrupt a statement that is already executing in another thread. +** The step must return SQLITE_INTERRUPT, and it must do so promptly rather +** than running the full join to completion. +*/ +static void test_inflight(sqlite3 *db){ + sqlite3_stmt *stmt; + Worker w; + printf("test_inflight: interrupt a step() already in flight\n"); + must_ok(db, sqlite3_prepare_v2(db, + "SELECT count(*) FROM t a, t b", -1, &stmt, 0), "prepare join"); + + w = run_and_interrupt(stmt, stmt_interrupt_cb, stmt); + + printf(" ... rc=%d (%s), ran %.0f ms\n", + w.rc, sqlite3_errstr(w.rc), w.elapsed_ms); + CHECK(w.rc==SQLITE_INTERRUPT, "step returned SQLITE_INTERRUPT"); + CHECK(w.elapsed_ms < DELAY_MS + REACT_MS, + "step aborted promptly (mid-execution, not at completion)"); + sqlite3_finalize(stmt); +} + +/* +** Test 2: statement-level granularity. Interrupting statement A must not set +** the connection-wide interrupt state, so an unrelated statement B on the same +** connection continues to run normally. +*/ +static void test_granularity(sqlite3 *db){ + sqlite3_stmt *a, *b; + Worker w; + int rc; + printf("test_granularity: interrupting A does not disturb the connection\n"); + must_ok(db, sqlite3_prepare_v2(db, + "SELECT count(*) FROM t a, t b", -1, &a, 0), "prepare A"); + must_ok(db, sqlite3_prepare_v2(db, "SELECT 1", -1, &b, 0), "prepare B"); + + w = run_and_interrupt(a, stmt_interrupt_cb, a); + CHECK(w.rc==SQLITE_INTERRUPT, "A returned SQLITE_INTERRUPT"); + CHECK(sqlite3_is_interrupted(db)==0, + "connection interrupt flag is NOT set (per-statement only)"); + + rc = sqlite3_step(b); + CHECK(rc==SQLITE_ROW, "unrelated statement B still runs to a row"); + sqlite3_finalize(a); + sqlite3_finalize(b); +} + +int main(int argc, char **argv){ + sqlite3 *db; + int rc; + (void)argc; (void)argv; + + rc = sqlite3_open_v2(":memory:", &db, + SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE|SQLITE_OPEN_FULLMUTEX, 0); + if( rc!=SQLITE_OK ){ + fprintf(stderr, "fatal: open: %s\n", sqlite3_errmsg(db)); + return 2; + } + + exec(db, "CREATE TABLE t(x);"); + { + char zSql[256]; + snprintf(zSql, sizeof(zSql), + "WITH RECURSIVE c(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM c WHERE i<%d)" + " INSERT INTO t SELECT i FROM c;", WORK_ROWS); + exec(db, zSql); + } + + test_inflight(db); + test_granularity(db); + + sqlite3_close(db); + + printf("\n%s\n", nFail==0 ? "PASS" : "FAIL"); + return nFail==0 ? 0 : 1; +} From 72c068f903a9f003e85d10d106516585815e85f5 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Tue, 2 Jun 2026 08:45:35 +0300 Subject: [PATCH 2/3] libsql-ffi: Update SQLite bundle --- .../SQLite3MultipleCiphers/src/sqlite3.c | 31 +++++++++++++++---- libsql-ffi/bundled/src/sqlite3.c | 31 +++++++++++++++---- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/libsql-ffi/bundled/SQLite3MultipleCiphers/src/sqlite3.c b/libsql-ffi/bundled/SQLite3MultipleCiphers/src/sqlite3.c index 988693f341..c286d77b4f 100644 --- a/libsql-ffi/bundled/SQLite3MultipleCiphers/src/sqlite3.c +++ b/libsql-ffi/bundled/SQLite3MultipleCiphers/src/sqlite3.c @@ -70,6 +70,7 @@ ** src/sqlite3ext.h ** src/sqliteInt.h ** src/status.c +** src/test1.c ** src/test2.c ** src/test3.c ** src/test8.c @@ -90201,7 +90202,7 @@ SQLITE_PRIVATE int sqlite3VdbeReset(Vdbe *p){ #ifdef SQLITE_DEBUG p->nWrite = 0; #endif - p->isInterrupted = 0; + AtomicStore(&p->isInterrupted, 0); /* Save profiling information from this VDBE run. */ @@ -93043,7 +93044,9 @@ void libsql_stmt_interrupt(sqlite3_stmt *pStmt){ (void)SQLITE_MISUSE_BKPT; return; } - v->isInterrupted = 1; + /* Set atomically: this may be called from a different thread than the one + ** executing the statement, mirroring sqlite3_interrupt(). */ + AtomicStore(&v->isInterrupted, 1); } /* @@ -93061,7 +93064,7 @@ SQLITE_API int sqlite3_step(sqlite3_stmt *pStmt){ return SQLITE_MISUSE_BKPT; } db = v->db; - if( v->isInterrupted ){ + if( AtomicLoad(&v->isInterrupted) ){ rc = SQLITE_INTERRUPT; v->rc = rc; db->errCode = rc; @@ -95105,6 +95108,12 @@ SQLITE_API int sqlite3_search_count = 0; */ #ifdef SQLITE_TEST SQLITE_API int sqlite3_interrupt_count = 0; +/* +** As above, but simulates a statement-level interrupt +** (libsql_stmt_interrupt()) on the statement that is currently executing, +** rather than a connection-wide sqlite3_interrupt(). +*/ +SQLITE_API int sqlite3_stmt_interrupt_count = 0; #endif /* @@ -95928,7 +95937,9 @@ SQLITE_PRIVATE int sqlite3VdbeExec( p->iCurrentTime = 0; assert( p->explain==0 ); db->busyHandler.nBusy = 0; - if( AtomicLoad(&db->u1.isInterrupted) ) goto abort_due_to_interrupt; + if( AtomicLoad(&db->u1.isInterrupted) || AtomicLoad(&p->isInterrupted) ){ + goto abort_due_to_interrupt; + } sqlite3VdbeIOTraceSql(p); #ifdef SQLITE_DEBUG sqlite3BeginBenignMalloc(); @@ -95997,6 +96008,12 @@ SQLITE_PRIVATE int sqlite3VdbeExec( sqlite3_interrupt(db); } } + if( sqlite3_stmt_interrupt_count>0 ){ + sqlite3_stmt_interrupt_count--; + if( sqlite3_stmt_interrupt_count==0 ){ + libsql_stmt_interrupt((sqlite3_stmt*)p); + } + } #endif /* Sanity checking on other operands */ @@ -96118,7 +96135,9 @@ case OP_Goto: { /* jump */ ** checks on every opcode. This helps sqlite3_step() to run about 1.5% ** faster according to "valgrind --tool=cachegrind" */ check_for_interrupt: - if( AtomicLoad(&db->u1.isInterrupted) ) goto abort_due_to_interrupt; + if( AtomicLoad(&db->u1.isInterrupted) || AtomicLoad(&p->isInterrupted) ){ + goto abort_due_to_interrupt; + } #ifndef SQLITE_OMIT_PROGRESS_CALLBACK /* Call the progress callback if it is configured and the required number ** of VDBE ops have been executed (either since this invocation of @@ -104463,7 +104482,7 @@ default: { /* This is really OP_Noop, OP_Explain */ ** flag. */ abort_due_to_interrupt: - assert( AtomicLoad(&db->u1.isInterrupted) ); + assert( AtomicLoad(&db->u1.isInterrupted) || AtomicLoad(&p->isInterrupted) ); rc = SQLITE_INTERRUPT; goto abort_due_to_error; } diff --git a/libsql-ffi/bundled/src/sqlite3.c b/libsql-ffi/bundled/src/sqlite3.c index 988693f341..c286d77b4f 100644 --- a/libsql-ffi/bundled/src/sqlite3.c +++ b/libsql-ffi/bundled/src/sqlite3.c @@ -70,6 +70,7 @@ ** src/sqlite3ext.h ** src/sqliteInt.h ** src/status.c +** src/test1.c ** src/test2.c ** src/test3.c ** src/test8.c @@ -90201,7 +90202,7 @@ SQLITE_PRIVATE int sqlite3VdbeReset(Vdbe *p){ #ifdef SQLITE_DEBUG p->nWrite = 0; #endif - p->isInterrupted = 0; + AtomicStore(&p->isInterrupted, 0); /* Save profiling information from this VDBE run. */ @@ -93043,7 +93044,9 @@ void libsql_stmt_interrupt(sqlite3_stmt *pStmt){ (void)SQLITE_MISUSE_BKPT; return; } - v->isInterrupted = 1; + /* Set atomically: this may be called from a different thread than the one + ** executing the statement, mirroring sqlite3_interrupt(). */ + AtomicStore(&v->isInterrupted, 1); } /* @@ -93061,7 +93064,7 @@ SQLITE_API int sqlite3_step(sqlite3_stmt *pStmt){ return SQLITE_MISUSE_BKPT; } db = v->db; - if( v->isInterrupted ){ + if( AtomicLoad(&v->isInterrupted) ){ rc = SQLITE_INTERRUPT; v->rc = rc; db->errCode = rc; @@ -95105,6 +95108,12 @@ SQLITE_API int sqlite3_search_count = 0; */ #ifdef SQLITE_TEST SQLITE_API int sqlite3_interrupt_count = 0; +/* +** As above, but simulates a statement-level interrupt +** (libsql_stmt_interrupt()) on the statement that is currently executing, +** rather than a connection-wide sqlite3_interrupt(). +*/ +SQLITE_API int sqlite3_stmt_interrupt_count = 0; #endif /* @@ -95928,7 +95937,9 @@ SQLITE_PRIVATE int sqlite3VdbeExec( p->iCurrentTime = 0; assert( p->explain==0 ); db->busyHandler.nBusy = 0; - if( AtomicLoad(&db->u1.isInterrupted) ) goto abort_due_to_interrupt; + if( AtomicLoad(&db->u1.isInterrupted) || AtomicLoad(&p->isInterrupted) ){ + goto abort_due_to_interrupt; + } sqlite3VdbeIOTraceSql(p); #ifdef SQLITE_DEBUG sqlite3BeginBenignMalloc(); @@ -95997,6 +96008,12 @@ SQLITE_PRIVATE int sqlite3VdbeExec( sqlite3_interrupt(db); } } + if( sqlite3_stmt_interrupt_count>0 ){ + sqlite3_stmt_interrupt_count--; + if( sqlite3_stmt_interrupt_count==0 ){ + libsql_stmt_interrupt((sqlite3_stmt*)p); + } + } #endif /* Sanity checking on other operands */ @@ -96118,7 +96135,9 @@ case OP_Goto: { /* jump */ ** checks on every opcode. This helps sqlite3_step() to run about 1.5% ** faster according to "valgrind --tool=cachegrind" */ check_for_interrupt: - if( AtomicLoad(&db->u1.isInterrupted) ) goto abort_due_to_interrupt; + if( AtomicLoad(&db->u1.isInterrupted) || AtomicLoad(&p->isInterrupted) ){ + goto abort_due_to_interrupt; + } #ifndef SQLITE_OMIT_PROGRESS_CALLBACK /* Call the progress callback if it is configured and the required number ** of VDBE ops have been executed (either since this invocation of @@ -104463,7 +104482,7 @@ default: { /* This is really OP_Noop, OP_Explain */ ** flag. */ abort_due_to_interrupt: - assert( AtomicLoad(&db->u1.isInterrupted) ); + assert( AtomicLoad(&db->u1.isInterrupted) || AtomicLoad(&p->isInterrupted) ); rc = SQLITE_INTERRUPT; goto abort_due_to_error; } From f29dd3a8c3681dad9ba6f8244dea86f97d985430 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Tue, 2 Jun 2026 08:53:29 +0300 Subject: [PATCH 3/3] Update Cargo.lock to 0.10.0-pre.3 The 0.10.0-pre.3 version bump (61d629a0ed) updated the workspace Cargo.toml versions but not Cargo.lock, which stayed at 0.10.0-pre.2. Any cargo invocation (including the c-bundle-validate CI job's 'cargo xtask build-bundled') rewrites the lockfile to match, leaving an uncommitted Cargo.lock change that fails the job's 'git diff --quiet' check. Regenerate the lockfile so it matches the committed Cargo.toml. --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9d411cd66c..c078f4b6d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2899,7 +2899,7 @@ dependencies = [ [[package]] name = "libsql" -version = "0.10.0-pre.2" +version = "0.10.0-pre.3" dependencies = [ "anyhow", "async-stream", @@ -2959,7 +2959,7 @@ dependencies = [ [[package]] name = "libsql-ffi" -version = "0.10.0-pre.2" +version = "0.10.0-pre.3" dependencies = [ "bindgen", "cc", @@ -2970,7 +2970,7 @@ dependencies = [ [[package]] name = "libsql-hrana" -version = "0.10.0-pre.2" +version = "0.10.0-pre.3" dependencies = [ "base64 0.21.7", "bytes", @@ -2981,7 +2981,7 @@ dependencies = [ [[package]] name = "libsql-rusqlite" -version = "0.10.0-pre.2" +version = "0.10.0-pre.3" dependencies = [ "bencher", "bitflags 2.6.0", @@ -3124,7 +3124,7 @@ dependencies = [ [[package]] name = "libsql-sys" -version = "0.10.0-pre.2" +version = "0.10.0-pre.3" dependencies = [ "bytes", "libsql-ffi", @@ -3155,7 +3155,7 @@ dependencies = [ [[package]] name = "libsql_replication" -version = "0.10.0-pre.2" +version = "0.10.0-pre.3" dependencies = [ "aes", "arbitrary",