diff --git a/Documentation/Index.md b/Documentation/Index.md index 44dc85ea..91bc4740 100644 --- a/Documentation/Index.md +++ b/Documentation/Index.md @@ -16,6 +16,7 @@ - [In-Memory Databases](#in-memory-databases) - [URI parameters](#uri-parameters) - [Thread-Safety](#thread-safety) + - [Closing a Connection](#closing-a-connection) - [Building Type-Safe SQL](#building-type-safe-sql) - [Expressions](#expressions) - [Compound Expressions](#compound-expressions) @@ -490,6 +491,22 @@ db.busyHandler({ tries in > errors, you may be trying to access the same database simultaneously from > multiple connections. +#### Closing a Connection + +Call `close()` when you need the database connection closed before the +`Connection` goes out of scope—for example, before removing a temporary +database file from disk. + +```swift +try db.close() +``` + +Release prepared statements, BLOB handles, and [backup](#online-database-backup) +objects before closing; otherwise `close()` may throw. + +> _Note:_ On iOS, call `close()` (and handle any errors) before deleting temporary +> database files to avoid filesystem warnings. + ## Building Type-Safe SQL diff --git a/Makefile b/Makefile index 0bde22d5..807e42ab 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ IOS_VERSION = 16.4 SWIFTLINT_VERSION=0.63.1 SWIFTLINT=bin/swiftlint-$(SWIFTLINT_VERSION) SWIFTLINT_URL=https://github.com/realm/SwiftLint/releases/download/$(SWIFTLINT_VERSION)/portable_swiftlint.zip -XCBEAUTIFY_VERSION=3.1.2 +XCBEAUTIFY_VERSION=3.2.1 XCBEAUTIFY=bin/xcbeautify-$(XCBEAUTIFY_VERSION) ifeq ($(shell uname), Linux) XCBEAUTIFY_PLATFORM=x86_64-unknown-linux-gnu.tar.xz diff --git a/Sources/SQLite/Core/Connection.swift b/Sources/SQLite/Core/Connection.swift index 7529c678..a6199204 100644 --- a/Sources/SQLite/Core/Connection.swift +++ b/Sources/SQLite/Core/Connection.swift @@ -85,7 +85,10 @@ public final class Connection { } } - public var handle: OpaquePointer { _handle! } + public var handle: OpaquePointer { + precondition(_handle != nil, "connection already closed") + return _handle! + } fileprivate var _handle: OpaquePointer? @@ -161,7 +164,7 @@ public final class Connection { } deinit { - sqlite3_close(handle) + try? close() } // MARK: - @@ -186,6 +189,26 @@ public final class Connection { Int(sqlite3_total_changes(handle)) } + /// Closes the database connection immediately. + /// Normally not needed: the underlying connection is automatically closed when `Connection` goes out of scope. + /// Callers should release or exhaust prepared statements, BLOB handles, and backup operations before closing. If + /// SQLite reports that the connection is still busy, this method throws and keeps the connection handle open. + public func close() throws { + try sync { + guard let handle = _handle else { + return + } + + let resultCode = sqlite3_close(handle) + if resultCode == SQLITE_OK { + _handle = nil + return + } + + try check(resultCode) + } + } + /// Whether or not the database will return extended error codes when errors are handled. public var usesExtendedErrorCodes: Bool = false { didSet { diff --git a/Tests/SQLiteTests/Core/ConnectionTests.swift b/Tests/SQLiteTests/Core/ConnectionTests.swift index 2da096bf..9b3534fd 100644 --- a/Tests/SQLiteTests/Core/ConnectionTests.swift +++ b/Tests/SQLiteTests/Core/ConnectionTests.swift @@ -71,6 +71,35 @@ class ConnectionTests: SQLiteTestCase { XCTAssertTrue(db.readonly) } + func test_close_closesConnectionAndIsIdempotent() throws { + let db = try Connection() + + try db.close() + try db.close() + } + + func test_close_whenPreparedStatementIsActiveThrowsBusyAndCanBeRetried() throws { + let db = try Connection() + // open statement + var statement: Statement? = try db.prepare("SELECT 1") + + XCTAssertThrowsError(try db.close()) { error in + if case SQLite.Result.error(_, let code, _) = error { + // there's an open statement + XCTAssertEqual(SQLITE_BUSY, code) + } else { + XCTFail("unexpected error: \(error)") + } + } + // db still operational + XCTAssertEqual(1, try db.scalar("SELECT 1") as? Int64) + // statement is closed, no more references + if statement != nil { + statement = nil + } + try db.close() + } + func test_changes_returnsZeroOnNewConnections() { XCTAssertEqual(0, db.changes) }