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; }