Skip to content

Add explicit deterministic close support for SQLite connections#1361

Open
RuiNelson wants to merge 4 commits into
stephencelis:masterfrom
RuiNelson:master
Open

Add explicit deterministic close support for SQLite connections#1361
RuiNelson wants to merge 4 commits into
stephencelis:masterfrom
RuiNelson:master

Conversation

@RuiNelson
Copy link
Copy Markdown

This PR adds an explicit Connection.close() API so callers can deterministically close a SQLite database connection before releasing or deleting the underlying database file.

Motivation

Some applications need to manage short-lived SQLite database files explicitly. In those cases, relying only on Connection.deinit can make file lifetime difficult to coordinate with external cleanup.

This is especially important when a connection may still have active SQLite resources, such as prepared statements. SQLite’s sqlite3_close() returns SQLITE_BUSY in that case and keeps the connection open. Without an explicit close API, callers cannot observe that failure or retry the close after releasing the remaining resources.

Solution

The new Connection.close() method:

  • Calls sqlite3_close() synchronously on the connection queue.
  • Sets the internal handle to nil only when SQLite reports SQLITE_OK.
  • Throws the SQLite error when close fails, preserving the live handle so callers can continue using the connection or retry closing it later.
  • Is idempotent when called after a successful close.
  • Is also used from deinit, preserving the previous automatic cleanup behavior.

Tests

Added regression coverage for the new behavior:

  • Verifies that close() succeeds and is idempotent.
  • Verifies that close() throws SQLITE_BUSY while a prepared statement is still active.
  • Verifies that the connection remains usable after a failed close attempt.
  • Verifies that close() succeeds after the active statement is released.

RuiNelson added 2 commits May 31, 2026 00:33
Add an explicit Connection.close() API that synchronously closes the underlying sqlite3 handle and only clears it after sqlite3_close returns SQLITE_OK.

Previously Connection.deinit called sqlite3_close directly and ignored its result. If SQLite still had internal statements, BLOB handles, or backup objects alive, sqlite3_close could return SQLITE_BUSY and leave the database connection open. That made the Swift object appear destroyed while SQLite still held file descriptors, which could trigger iOS filesystem warnings when temporary database files were unlinked shortly afterwards.

The deinitializer now delegates to close(), preserving the existing automatic cleanup path while avoiding silent handle leaks when explicit close succeeds.
Add regression coverage for the explicit Connection.close() API.

The new tests verify that close() is idempotent, reports SQLITE_BUSY while a prepared statement is still active, leaves the connection usable after a failed close attempt, and succeeds once the statement has been released.
@jberkel
Copy link
Copy Markdown
Collaborator

jberkel commented Jun 4, 2026

Thanks for the PR! Can you document this change in Documentation/Index.md and rebase the PR against the latest master so that the tests get a chance to run?

@RuiNelson
Copy link
Copy Markdown
Author

Thanks for the PR! Can you document this change in Documentation/Index.md and rebase the PR against the latest master so that the tests get a chance to run?

Done.

I tried to maintain the level of verbosity and tone of the rest of the document.


let resultCode = sqlite3_close(handle)
if resultCode == SQLITE_OK {
_handle = nil
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will put the Connection object in a state where most subsequent operations will fail while force unwrapping the handle (line 88).

The intention of having the sqlit3_close happen in the deinit was probably to avoid this: either the object is constructed, and usable, or it's no longer in scope (and closed). Being able to close this explicitly puts the connection in a gray zone.

Perhaps the code unwrapping the handle could make this explicit:

public var handle: OpaquePointer {
    assert(_handle != nil, "connection closed")
    return _handle!
}

The code will still fail, but the error message will be clearer.

@jberkel
Copy link
Copy Markdown
Collaborator

jberkel commented Jun 6, 2026

Thanks for the PR! Can you document this change in Documentation/Index.md and rebase the PR against the latest master so that the tests get a chance to run?

Done.

I tried to maintain the level of verbosity and tone of the rest of the document.

Sounds like something an AI would say when prompted to write documentation :)

@RuiNelson
Copy link
Copy Markdown
Author

Thanks for the PR! Can you document this change in Documentation/Index.md and rebase the PR against the latest master so that the tests get a chance to run?

Done.
I tried to maintain the level of verbosity and tone of the rest of the document.

Sounds like something an AI would say when prompted to write documentation :)

Actually, it was what I told my AI agent to translate my Portuguese draft to English :o)

Or maybe I'm a machine inside an human-like body that was programmed to think it is human. We'll never know!

@jberkel
Copy link
Copy Markdown
Collaborator

jberkel commented Jun 6, 2026

Thanks for the PR! Can you document this change in Documentation/Index.md and rebase the PR against the latest master so that the tests get a chance to run?

Done.
I tried to maintain the level of verbosity and tone of the rest of the document.

Sounds like something an AI would say when prompted to write documentation :)

Actually, it was what I told my AI agent to translate my Portuguese draft to English :o)

Or maybe I'm a machine inside an human-like body that was programmed to think it is human. We'll never know!

Haha, ok! What do you think about the other suggestions? A more explicit failure when the connection is closed?

@RuiNelson
Copy link
Copy Markdown
Author

I was thinking about throwing an error like, but I need to check the codebase first...

func getHandle() throws(ConnectionAlreadyClosed) {
 guard let handle else {
  throw ConnectionAlreadyClosed()
 }
 
 return handle
}


// and then, on each method:
let handle = try getHandle()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants