From 86199831cfeb5c652e70944ea04f0175b50f11d2 Mon Sep 17 00:00:00 2001 From: Henrik Maier Date: Sat, 15 Aug 2026 13:52:31 +1000 Subject: [PATCH] drvgettable(): fix leaked SQLite statement when SQL_ATTR_MAX_ROWS cap is hit If a statement's row-count cap (SQL_ATTR_MAX_ROWS) is reached before the result set is exhausted, the fetch loop broke out without finalizing or resetting the active sqlite3_stmt. The very next outer- loop iteration then overwrote the only reference to it (tres.stmt = NULL), orphaning a still-open statement that keeps holding its SQLite lock for as long as the connection stays open -- blocking every other process from writing to the file, independent of any BEGIN/COMMIT or autocommit state. Route the max_rows early-exit through the same finalize-or-reset cleanup already used by the loop's normal completion path instead of skipping it, so the statement (and its lock) is released immediately when the cap is hit. --- sqlite3odbc.c | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/sqlite3odbc.c b/sqlite3odbc.c index 5f5959d..bb31357 100644 --- a/sqlite3odbc.c +++ b/sqlite3odbc.c @@ -1587,6 +1587,37 @@ drvgettable(STMT *s, const char *sql, char ***resp, int *nrowp, ncol = sqlite3_column_count(tres.stmt); while (1) { if (s->max_rows && tres.nrow >= s->max_rows) { + /* + * BUGFIX: previously this broke out of the loop + * without finalizing or resetting tres.stmt. The + * outer loop then unconditionally overwrites + * tres.stmt with NULL before preparing the next + * statement, silently orphaning the still-open + * SQLite statement handle -- no C-level reference is + * left to ever finalize it. An orphaned, unfinalized + * statement keeps holding its SQLite-level lock for + * as long as the connection stays open, completely + * independent of any BEGIN/COMMIT/autocommit state. + * This matches a client hitting a row-count cap + * (SQL_ATTR_MAX_ROWS) on a table larger than that + * cap: the resulting held lock would block every + * other process from writing to the file until the + * connection holding the leaked statement fully + * closes. Route through the same + * finalize-or-reset-then-advance cleanup used by the + * normal completion path below instead of skipping + * it. + */ + if (keep) { + dbtraceapi(d, "sqlite3_reset", 0); + rc = sqlite3_reset(tres.stmt); + s->s3stmt_noreset = 1; + } else { + dbtraceapi(d, "sqlite3_finalize", 0); + rc = sqlite3_finalize(tres.stmt); + } + tres.stmt = 0; + sql = NULL; rc = SQLITE_OK; break; }