Skip to content

Commit 2d7649d

Browse files
authored
Merge pull request #719 from katafrakt/close-after-failed-open
Close connection immediately after failed open
2 parents 3b6982e + c8ed6af commit 2d7649d

3 files changed

Lines changed: 42 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# sqlite3-ruby Changelog
22

3+
## next / unreleased
4+
5+
### Improved
6+
7+
- When `Database.new` fails to open the database file, the underlying sqlite3 connection handle is now closed immediately instead of waiting for the garbage collector to clean it up. #719 @katafrakt
8+
9+
310
## 2.9.5 / 2026-06-07
411

512
### Dependencies

ext/sqlite3/database.c

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,13 @@ rb_sqlite3_open_v2(VALUE self, VALUE file, VALUE mode, VALUE zvfs)
153153
NIL_P(zvfs) ? NULL : StringValuePtr(zvfs)
154154
);
155155

156-
CHECK(ctx->db, status);
156+
if (status != SQLITE_OK) {
157+
char *msg = sqlite3_mprintf("%s", sqlite3_errmsg(ctx->db));
158+
sqlite3_close_v2(ctx->db);
159+
ctx->db = NULL;
160+
CHECK_MSG(ctx->db, status, msg);
161+
}
162+
157163
if (flags & SQLITE_OPEN_READONLY) {
158164
ctx->flags |= SQLITE3_RB_DATABASE_READONLY;
159165
}
@@ -941,7 +947,12 @@ rb_sqlite3_open16(VALUE self, VALUE file)
941947
// so we do not ever set SQLITE3_RB_DATABASE_READONLY in ctx->flags
942948
status = sqlite3_open16(utf16_string_value_ptr(file), &ctx->db);
943949

944-
CHECK(ctx->db, status)
950+
if (status != SQLITE_OK) {
951+
char *msg = sqlite3_mprintf("%s", sqlite3_errmsg(ctx->db));
952+
sqlite3_close_v2(ctx->db);
953+
ctx->db = NULL;
954+
CHECK_MSG(ctx->db, status, msg);
955+
}
945956

946957
return INT2NUM(status);
947958
}

test/test_resource_cleanup.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,28 @@ def test_cleanup_unclosed_statement_object
1717
end
1818
end
1919

20+
# These tests exercise the failure branches of open_v2 and open16, which immediately close the
21+
# sqlite3 handle and reclaim resources before raising.
22+
#
23+
# Under valgrind these would catch any memory problems in the close-on-failure code, but they do
24+
# NOT directly test that resources are immediately recovered. If we didn't close immediately,
25+
# the GC finalizer would still eventually close any leftover handles if and when the database
26+
# object is GCed. Beacuse we run ruby with free-at-exit with valgrind, directly testing the
27+
# immediate close is hard. But I'm fine with that tradeoff.
28+
def test_cleanup_failed_open_v2
29+
exception = assert_raise(SQLite3::CantOpenException) do
30+
SQLite3::Database.new("/nonexistent/foo.db")
31+
end
32+
assert_equal("unable to open database file", exception.message)
33+
end
34+
35+
def test_cleanup_failed_open16
36+
exception = assert_raise(SQLite3::CantOpenException) do
37+
SQLite3::Database.new("/nonexistent/foo.db".encode(Encoding::UTF_16LE))
38+
end
39+
assert_equal("unable to open database file", exception.message)
40+
end
41+
2042
# # this leaks the result set
2143
# def test_cleanup_unclosed_resultset_object
2244
# db = SQLite3::Database.new(':memory:')

0 commit comments

Comments
 (0)