Skip to content

Commit

Permalink
Lots of documentation comments. Reorganize SQLiteConnection a little.…
Browse files Browse the repository at this point in the history
… SQLiteDatabase is Sendable.
  • Loading branch information
gwynne committed May 10, 2024
1 parent 6f72ea1 commit 84c80bd
Show file tree
Hide file tree
Showing 3 changed files with 291 additions and 51 deletions.
193 changes: 167 additions & 26 deletions Sources/SQLiteNIO/SQLiteConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,33 @@ import NIOPosix
import CSQLite
import Logging

/// A wrapper for the `OpaquePointer` used to represent an open `sqlite3` handle.
///
/// This wrapper serves two purposes:
///
/// - Silencing `Sendable` warnings relating to use of the pointer, and
/// - Preventing confusion with other C types which import as opaque pointers.
///
/// The use of `@unchecked Sendable` is safe for this type because:
///
/// - We ensure that access to the raw handle only ever takes place while running on an `NIOThreadPool`.
/// This does not prevent concurrent access to the handle from multiple threads, but does tend to limit
/// the possibility of misuse (and of course prevents CPU-bound work from ending up on an event loop).
/// - The embedded SQLite is built with `SQLITE_THREADSAFE=1` (serialized mode, permitting safe use of a
/// given connection handle simultaneously from multiple threads).
/// - We include `SQLITE_OPEN_FULLMUTEX` when calling `sqlite_open_v2()`, guaranteeing the use of the
/// serialized threading mode for each connection even if someone uses `sqlite3_config()` to make the
/// less strict multithreaded mode the default.
///
/// And finally, the use of `@unchecked` in particular is justified because:
///
/// 1. We need to be able to mutate the value in order to make it `nil` when the connection it represented
/// is closed. We use the `nil` value as a sentinel by which we determine a connection's validity. Also,
/// _not_ `nil`-ing it out would leave a dangling/freed pointer in place, which is just begging for a
/// segfault.
/// 2. An `OpaquePointer` can not be natively `Sendable`, by definition; it's opaque! The `@unchecked`
/// annotation is how we tell the compiler "we've taken the appropriate precautions to make moving
/// values of this type between isolation regions safe".
final class SQLiteConnectionHandle: @unchecked Sendable {
var raw: OpaquePointer?

Expand All @@ -11,30 +38,59 @@ final class SQLiteConnectionHandle: @unchecked Sendable {
}
}

/// Represents a single open connection to an SQLite database, either on disk or in memory.
public final class SQLiteConnection: SQLiteDatabase, Sendable {
/// Available SQLite storage methods.
/// The possible storage types for an SQLite database.
public enum Storage: Equatable, Sendable {
/// In-memory storage. Not persisted between application launches.
/// Good for unit testing or caching.
/// An SQLite database stored entirely in memory.
///
/// In-memory databases persist only so long as the connection to them is open, and are not shared
/// between processes. In addition, because this package builds the sqlite3 amalgamation with the
/// recommended `SQLITE_OMIT_SHARED_CACHE` option, it is not possible to open multiple connections
/// to a single in-memory database; use a temporary file instead.
///
/// In-memory databases are useful for unit testing or caching purposes.
case memory

/// File-based storage, persisted between application launches.
/// An SQLite database stored in a file at the specified path.
///
/// If a relative path is specified, it is interpreted relative to the current working directory of the
/// current process (e.g. `NIOFileSystem.shared.currentWorkingDirectory`) at the time of establishing
/// the connection. It is strongly recommended that users always use absolute paths whenever possible.
///
/// File-based databases persist as long as the files representing them on disk does, and can be opened
/// multiple times within the same process or even by multiple processes if configured properly.
case file(path: String)
}

// See `SQLiteDatabase.eventLoop`.
public let eventLoop: any EventLoop

// See `SQLiteDatabase.logger`.
public let logger: Logger

let handle: SQLiteConnectionHandle
private let threadPool: NIOThreadPool
/// Return the version of the embedded libsqlite3 as a 32-bit integer value.
///
/// The value is laid out identicallly to [the `SQLITE_VERSION_NUMBER` constant](c_source_id).
///
/// [c_source_id]: https://sqlite.org/c3ref/c_source_id.html
public static func libraryVersion() -> Int32 {
sqlite_nio_sqlite3_libversion_number()
}

public var isClosed: Bool {
self.handle.raw == nil
/// Return the version of the embedded libsqlite3 as a string.
///
/// The string is formatted identically to [the `SQLITE_VERSION` constant](c_source_id).
///
/// [c_source_id]: https://sqlite.org/c3ref/c_source_id.html
public static func libraryVersionString() -> String {
String(cString: sqlite_nio_sqlite3_libversion())
}


/// Open a new connection to an SQLite database.
///
/// This is equivalent to invoking ``open(storage:threadPool:logger:on:)-64n3x`` using the
/// `NIOThreadPool` and `MultiThreadedEventLoopGroup` singletons. This is the recommended configuration
/// for all users.
///
/// - Parameters:
/// - storage: Specifies the location of the database for the connection. See ``Storage`` for details.
/// - logger: The logger used by the connection. Defaults to a new `Logger`.
/// - Returns: A future whose value on success is a new connection object.
public static func open(
storage: Storage = .memory,
logger: Logger = .init(label: "codes.vapor.sqlite")
Expand All @@ -47,6 +103,14 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
)
}

/// Open a new connection to an SQLite database.
///
/// - Parameters:
/// - storage: Specifies the location of the database for the connection. See ``Storage`` for details.
/// - threadPool: An `NIOThreadPool` used to execute all libsqlite3 API calls for this connection.
/// - logger: The logger used by the connection. Defaults to a new `Logger`.
/// - eventLoop: An `EventLoop` to associate with the connection for creating futures.
/// - Returns: A future whose value on success is a new connection object.
public static func open(
storage: Storage = .memory,
threadPool: NIOThreadPool,
Expand All @@ -58,7 +122,14 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
}
}

private static func openInternal(storage: Storage, threadPool: NIOThreadPool, logger: Logger, eventLoop: any EventLoop) throws -> SQLiteConnection {
/// The underlying implementation of ``open(storage:threadPool:logger:on:)-64n3x`` and
/// ``open(storage:threadPool:logger:on:)-3m3lb``.
private static func openInternal(
storage: Storage,
threadPool: NIOThreadPool,
logger: Logger,
eventLoop: any EventLoop
) throws -> SQLiteConnection {
let path: String
switch storage {
case .memory: path = ":memory:"
Expand All @@ -78,11 +149,24 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
throw SQLiteError(reason: .init(statusCode: busyRet), message: "Failed to set busy handler for SQLite database at \(path)")
}

logger.debug("Connected to sqlite db: \(path)")
logger.debug("Connected to sqlite database", metadata: ["path": .string(path)])
return SQLiteConnection(handle: handle, threadPool: threadPool, logger: logger, on: eventLoop)
}

init(
// See `SQLiteDatabase.eventLoop`.
public let eventLoop: any EventLoop

// See `SQLiteDatabase.logger`.
public let logger: Logger

/// The underlying `sqlite3` connection handle.
let handle: SQLiteConnectionHandle

/// The thread pool used by this connection when calling libsqlite3 APIs.
private let threadPool: NIOThreadPool

/// Initialize a new ``SQLiteConnection``. Internal use only.
private init(
handle: OpaquePointer?,
threadPool: NIOThreadPool,
logger: Logger,
Expand All @@ -94,31 +178,39 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
self.eventLoop = eventLoop
}

/// Returns the most recent error message from the connection as a string.
///
/// This is only valid until another operation is performed on the connection; watch out for races.
var errorMessage: String? {
sqlite_nio_sqlite3_errmsg(self.handle.raw).map { String(cString: $0) }
}

public static func libraryVersion() -> Int32 {
sqlite_nio_sqlite3_libversion_number()
}

public static func libraryVersionString() -> String {
String(cString: sqlite_nio_sqlite3_libversion())
/// `false` if the connection is valid, `true` if not.
public var isClosed: Bool {
self.handle.raw == nil
}


/// Returns the last value generated by auto-increment functionality (either the version implied by
/// `INTEGER PRIMARY KEY` or that of the explicit `AUTO_INCREMENT` modifier) on this database.
///
/// Only valid until the next operation is performed on the connection; watch out for races.
///
/// - Returns: A future containing the most recently inserted rowid value.
public func lastAutoincrementID() -> EventLoopFuture<Int> {
self.threadPool.runIfActive(eventLoop: self.eventLoop) {
numericCast(sqlite_nio_sqlite3_last_insert_rowid(self.handle.raw))
}
}

// See `SQLiteDatabase.withConnection(_:)`.
@preconcurrency
public func withConnection<T>(
_ closure: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture<T>
) -> EventLoopFuture<T> {
closure(self)
}

// See `SQLiteDatabase.query(_:_:logger:_:)`.
@preconcurrency
public func query(
_ query: String,
Expand Down Expand Up @@ -150,33 +242,57 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
return promise.futureResult
}

/// Close the connection and invalidate its handle.
///
/// No further operations may be performed on the connection after calling this method.
///
/// - Returns: A future indicating completion of connection closure.
public func close() -> EventLoopFuture<Void> {
self.threadPool.runIfActive(eventLoop: self.eventLoop) {
sqlite_nio_sqlite3_close(self.handle.raw)
self.handle.raw = nil
}
}


/// Install the provided ``SQLiteCustomFunction`` on the connection.
///
/// - Parameter customFunction: The function to install.
/// - Returns: A future indicating completion of the install operation.
public func install(customFunction: SQLiteCustomFunction) -> EventLoopFuture<Void> {
self.logger.trace("Adding custom function \(customFunction.name)")
return self.threadPool.runIfActive(eventLoop: self.eventLoop) {
try customFunction.install(in: self)
}
}

/// Uninstall the provided ``SQLiteCustomFunction`` from the connection.
///
/// - Parameter customFunction: The function to remove.
/// - Returns: A future indicating completion of the uninstall operation.
public func uninstall(customFunction: SQLiteCustomFunction) -> EventLoopFuture<Void> {
self.logger.trace("Removing custom function \(customFunction.name)")
return self.threadPool.runIfActive(eventLoop: self.eventLoop) {
try customFunction.uninstall(in: self)
}
}

/// Deinitializer for ``SQLiteConnection``.
deinit {
assert(self.handle.raw == nil, "SQLiteConnection was not closed before deinitializing")
}
}

extension SQLiteConnection {
/// Open a new connection to an SQLite database.
///
/// This is equivalent to invoking ``open(storage:threadPool:logger:on:)-3m3lb`` using the
/// `NIOThreadPool` and `MultiThreadedEventLoopGroup` singletons. This is the recommended configuration
/// for all users.
///
/// - Parameters:
/// - storage: Specifies the location of the database for the connection. See ``Storage`` for details.
/// - logger: The logger used by the connection. Defaults to a new `Logger`.
/// - Returns: A future whose value on success is a new connection object.
public static func open(
storage: Storage = .memory,
logger: Logger = .init(label: "codes.vapor.sqlite")
Expand All @@ -189,6 +305,14 @@ extension SQLiteConnection {
)
}

/// Open a new connection to an SQLite database.
///
/// - Parameters:
/// - storage: Specifies the location of the database for the connection. See ``Storage`` for details.
/// - threadPool: An `NIOThreadPool` used to execute all libsqlite3 API calls for this connection.
/// - logger: The logger used by the connection. Defaults to a new `Logger`.
/// - eventLoop: An `EventLoop` to associate with the connection for creating futures.
/// - Returns: A new connection object.
public static func open(
storage: Storage = .memory,
threadPool: NIOThreadPool,
Expand All @@ -200,18 +324,26 @@ extension SQLiteConnection {
}
}

/// Returns the last value generated by auto-increment functionality (either the version implied by
/// `INTEGER PRIMARY KEY` or that of the explicit `AUTO_INCREMENT` modifier) on this database.
///
/// Only valid until the next operation is performed on the connection; watch out for races.
///
/// - Returns: The most recently inserted rowid value.
public func lastAutoincrementID() async throws -> Int {
try await self.threadPool.runIfActive {
numericCast(sqlite_nio_sqlite3_last_insert_rowid(self.handle.raw))
}
}

/// Concurrency-aware variant of ``withConnection(_:)-8cmxp``.
public func withConnection<T>(
_ closure: @escaping @Sendable (SQLiteConnection) async throws -> T
) async throws -> T {
try await closure(self)
}

/// Concurrency-aware variant of ``query(_:_:_:)-etrj``.
public func query(
_ query: String,
_ binds: [SQLiteData],
Expand All @@ -220,20 +352,29 @@ extension SQLiteConnection {
try await self.query(query, binds, onRow).get()
}

/// Close the connection and invalidate its handle.
///
/// No further operations may be performed on the connection after calling this method.
public func close() async throws {
try await self.threadPool.runIfActive {
sqlite_nio_sqlite3_close(self.handle.raw)
self.handle.raw = nil
}
}

/// Install the provided ``SQLiteCustomFunction`` on the connection.
///
/// - Parameter customFunction: The function to install.
public func install(customFunction: SQLiteCustomFunction) async throws {
self.logger.trace("Adding custom function \(customFunction.name)")
return try await self.threadPool.runIfActive {
try customFunction.install(in: self)
}
}

/// Uninstall the provided ``SQLiteCustomFunction`` from the connection.
///
/// - Parameter customFunction: The function to remove.
public func uninstall(customFunction: SQLiteCustomFunction) async throws {
self.logger.trace("Removing custom function \(customFunction.name)")
return try await self.threadPool.runIfActive {
Expand Down
Loading

0 comments on commit 84c80bd

Please sign in to comment.