Skip to content

Commit

Permalink
Merge pull request #8 from Alexander-Ignition/big-refactor
Browse files Browse the repository at this point in the history
Big refactor
  • Loading branch information
Alexander-Ignition authored Jan 1, 2025
2 parents 574a04c + 903424a commit e3f2447
Show file tree
Hide file tree
Showing 13 changed files with 697 additions and 496 deletions.
2 changes: 1 addition & 1 deletion .swift-format
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@
"UseSynthesizedInitializer" : false,
"UseTripleSlashForDocumentationComments" : true,
"UseWhereClausesInForLoops" : false,
"ValidateDocumentationComments" : false
"ValidateDocumentationComments" : true
}
}
16 changes: 16 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,19 @@ $(OUTPUD_DIR)/Docs: $(DOCC_ARCHIVE)
xcrun docc process-archive transform-for-static-hosting $^ \
--hosting-base-path $(TARGET_NAME) \
--output-path $@

# MARK: - DocC preview

DOC_CATALOG = Sources/$(TARGET_NAME)/$(TARGET_NAME).docc
SYMBOL_GRAPHS = $(OUTPUD_DIR)/symbol-graphs

$(SYMBOL_GRAPHS):
swift build --target $(TARGET_NAME) -Xswiftc -emit-symbol-graph -Xswiftc -emit-symbol-graph-dir -Xswiftc $@

$(OUTPUD_DIR)/doc-preview: $(DOC_CATALOG) $(SYMBOL_GRAPHS)
xcrun docc preview $(DOC_CATALOG) \
--fallback-display-name $(TARGET_NAME) \
--fallback-bundle-identifier org.swift.$(TARGET_NAME) \
--fallback-bundle-version 1.0.0 \
--additional-symbol-graph-dir $(SYMBOL_GRAPHS) \
--output-path $@
201 changes: 96 additions & 105 deletions Sources/SQLyra/Database.swift
Original file line number Diff line number Diff line change
@@ -1,147 +1,138 @@
import SQLite3

/// SQLite database.
public final class Database {
/// Execution callback type.
///
/// - SeeAlso: `Database.execute(_:handler:)`.
public typealias ExecutionHandler = (_ row: [String: String]) -> Void

public struct OpenOptions: OptionSet {
public final class Database: DatabaseHandle {
/// Database open options.
public struct OpenOptions: OptionSet, Sendable {
/// SQLite flags for opening a database connection.
public let rawValue: Int32

public init(rawValue: Int32) {
self.rawValue = rawValue
}

public init(_ rawValue: Int32) {
self.rawValue = rawValue
}
// MARK: - Required

/// The database is created if it does not already exist.
public static let create = OpenOptions(rawValue: SQLITE_OPEN_CREATE)

/// The database is opened for reading and writing if possible, or reading only if the file is write protected by the operating system.
///
/// In either case the database must already exist, otherwise an error is returned. For historical reasons,
/// if opening in read-write mode fails due to OS-level permissions, an attempt is made to open it in read-only mode.
public static let readwrite = OpenOptions(rawValue: SQLITE_OPEN_READWRITE)

/// The database is opened in read-only mode. If the database does not already exist, an error is returned.
public static let readonly = OpenOptions(rawValue: SQLITE_OPEN_READONLY)

// MARK: - Addition

/// The database will be opened as an in-memory database.
///
/// The database is named by the "filename" argument for the purposes of cache-sharing,
/// if shared cache mode is enabled, but the "filename" is otherwise ignored.
public static let memory = OpenOptions(rawValue: SQLITE_OPEN_MEMORY)

/// The database connection comes up in "extended result code mode".
public static let extendedResultCode = OpenOptions(rawValue: SQLITE_OPEN_EXRESCODE)

/// The filename can be interpreted as a URI if this flag is set.
public static let uri = OpenOptions(rawValue: SQLITE_OPEN_URI)

/// The database filename is not allowed to contain a symbolic link.
public static let noFollow = OpenOptions(rawValue: SQLITE_OPEN_NOFOLLOW)

public static var readonly: OpenOptions { .init(SQLITE_OPEN_READONLY) }
public static var readwrite: OpenOptions { .init(SQLITE_OPEN_READWRITE) }
public static var create: OpenOptions { .init(SQLITE_OPEN_CREATE) }
public static var uri: OpenOptions { .init(SQLITE_OPEN_URI) }
public static var memory: OpenOptions { .init(SQLITE_OPEN_MEMORY) }
public static var noMutex: OpenOptions { .init(SQLITE_OPEN_NOMUTEX) }
public static var fullMutex: OpenOptions { .init(SQLITE_OPEN_FULLMUTEX) }
public static var sharedCache: OpenOptions { .init(SQLITE_OPEN_SHAREDCACHE) }
public static var privateCache: OpenOptions { .init(SQLITE_OPEN_PRIVATECACHE) }
// MARK: - Threading modes

/// The new database connection will use the "multi-thread" threading mode.
///
/// This means that separate threads are allowed to use SQLite at the same time, as long as each thread is using a different database connection.
///
/// [Using SQLite in multi-threaded Applications](https://www.sqlite.org/threadsafe.html)
public static let noMutex = OpenOptions(rawValue: SQLITE_OPEN_NOMUTEX)

/// The new database connection will use the "serialized" threading mode.
///
/// This means the multiple threads can safely attempt to use the same database connection at the same time.
/// (Mutexes will block any actual concurrency, but in this mode there is no harm in trying.)
///
/// [Using SQLite in multi-threaded Applications](https://www.sqlite.org/threadsafe.html)
public static let fullMutex = OpenOptions(rawValue: SQLITE_OPEN_FULLMUTEX)

// MARK: - Cache modes

/// The database is opened shared cache enabled.
///
/// - Warning: The use of shared cache mode is discouraged and hence shared cache capabilities may be omitted
/// from many builds of SQLite. In such cases, this option is a no-op.
///
/// [SQLite Shared-Cache mode](https://www.sqlite.org/sharedcache.html)
public static let sharedCache = OpenOptions(rawValue: SQLITE_OPEN_SHAREDCACHE)

/// The database is opened shared cache disabled.
///
/// [SQLite Shared-Cache mode](https://www.sqlite.org/sharedcache.html)
public static let privateCache = OpenOptions(rawValue: SQLITE_OPEN_PRIVATECACHE)
}

/// SQLite db handle.
private(set) var db: OpaquePointer!

/// Absolute path to database file.
public var path: String { sqlite3_db_filename(db, nil).string ?? "" }
/// Return the filename for a database connection.
///
/// If database is a temporary or in-memory database, then this function will return either a nil or an empty string.
/// - SeeAlso: ``Database/OpenOptions/memory``
public var filename: String? { sqlite3_db_filename(db, nil).string }

/// Determine if a database is read-only.
///
/// - SeeAlso: `OpenOptions.readonly`.
/// - SeeAlso: ``Database/OpenOptions/readonly``
public var isReadonly: Bool { sqlite3_db_readonly(db, nil) == 1 }

/// Opening a new database connection.
///
/// - Parameters:
/// - path: Relative or absolute path to the database file.
/// - options: Database open options.
/// - filename: Relative or absolute path to the database file.
/// - options: The options parameter must include, at a minimum, one of the following three option combinations:
/// ``Database/OpenOptions/readonly``, ``Database/OpenOptions/readwrite``, ``Database/OpenOptions/create``.
/// - Returns: A new database connection.
/// - Throws: `DatabaseError`.
public static func open(at path: String, options: OpenOptions = []) throws -> Database {
/// - Throws: ``DatabaseError``
public static func open(at filename: String, options: OpenOptions = []) throws -> Database {
let database = Database()

let code = sqlite3_open_v2(path, &database.db, options.rawValue, nil)
try database.check(code)

return database
let code = sqlite3_open_v2(filename, &database.db, options.rawValue, nil)
return try database.check(code)
}

/// Use `Database.open(at:options:)`.
/// Use ``Database/open(at:options:)``.
private init() {}

deinit {
let code = sqlite3_close_v2(db)
assert(code == SQLITE_OK, "sqlite3_close_v2(): \(code)")
}

/// Run multiple statements of SQL.
/// One-step query execution Interface.
///
/// - Parameter sql: statements.
/// - Throws: `DatabaseError`.
public func execute(_ sql: String) throws {
let status = sqlite3_exec(db, sql, nil, nil, nil)
try check(status)
}

/// Run multiple statements of SQL with row handler.
/// The convenience wrapper around ``Database/prepare(_:)`` and ``PreparedStatement``,
/// that allows an application to run multiple statements of SQL without having to use a lot code.
///
/// - Parameters:
/// - sql: statements.
/// - handler: Table row handler.
/// - Throws: `DatabaseError`.
public func execute(_ sql: String, handler: @escaping ExecutionHandler) throws {
let context = ExecutionContext(handler)
let ctx = Unmanaged.passUnretained(context).toOpaque()
let status = sqlite3_exec(db, sql, readRow, ctx, nil)
try check(status)
}

/// Compiling an SQL statement.
public func prepare(_ sql: String, _ parameters: SQLParameter?...) throws -> PreparedStatement {
try prepare(sql, parameters: parameters)
/// - Parameter sql: UTF-8 encoded, semicolon-separate SQL statements to be evaluated.
/// - Throws: ``DatabaseError``
public func execute(_ sql: String) throws {
try check(sqlite3_exec(db, sql, nil, nil, nil))
}

/// Compiling an SQL statement.
public func prepare(_ sql: String, parameters: [SQLParameter?]) throws -> PreparedStatement {
var stmt: OpaquePointer!
let code = sqlite3_prepare_v2(db, sql, -1, &stmt, nil)
try check(code)
let statement = PreparedStatement(stmt: stmt)
try statement.bind(parameters: parameters)
return statement
}

/// Check result code.
///
/// - Throws: `DatabaseError` if code not ok.
private func check(_ code: Int32) throws {
if code != SQLITE_OK {
throw DatabaseError(code: code, database: self)
}
}

}

private final class ExecutionContext {
let handler: Database.ExecutionHandler

init(_ handler: @escaping Database.ExecutionHandler) {
self.handler = handler
}
}

private func readRow(
ctx: UnsafeMutableRawPointer?,
argc: Int32,
argv: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?,
columns: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
) -> Int32 {
guard let ctx, let argv, let columns else {
return SQLITE_OK
}
let count = Int(argc)
var row = [String: String](minimumCapacity: count)

for index in 0..<count {
guard let ptr = columns.advanced(by: index).pointee else {
continue
}
let name = String(cString: ptr)
if let value = argv.advanced(by: index).pointee {
row[name] = String(cString: value)
}
/// To execute an SQL statement, it must first be compiled into a byte-code program using one of these routines.
/// Or, in other words, these routines are constructors for the prepared statement object.
///
/// - Parameter sql: The statement to be compiled, encoded as UTF-8.
/// - Returns: A compiled prepared statement that can be executed.
/// - Throws: ``DatabaseError``
public func prepare(_ sql: String) throws -> PreparedStatement {
var stmt: OpaquePointer!
try check(sqlite3_prepare_v2(db, sql, -1, &stmt, nil))
return PreparedStatement(stmt: stmt)
}
let context = Unmanaged<ExecutionContext>.fromOpaque(ctx).takeUnretainedValue()
context.handler(row)
return SQLITE_OK
}
51 changes: 38 additions & 13 deletions Sources/SQLyra/DatabaseError.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Foundation
import SQLite3

/// SQLite database error.
Expand All @@ -6,34 +7,58 @@ public struct DatabaseError: Error, Equatable, Hashable {
public let code: Int32

/// A short error description.
public var message: String
public var message: String?

/// A complete sentence (or more) describing why the operation failed.
public let reason: String
public let details: String?

/// A new database error.
///
/// - Parameters:
/// - code: failed result code.
/// - message: A short error description.
/// - reason: A complete sentence (or more) describing why the operation failed.
public init(code: Int32, message: String, reason: String) {
/// - details: A complete sentence (or more) describing why the operation failed.
public init(code: Int32, message: String, details: String) {
self.code = code
self.message = message
self.reason = reason
self.details = details
}

init(code: Int32, database: Database) {
self.init(code: code, db: database.db)
init(code: Int32, db: OpaquePointer?) {
self.code = code
self.message = sqlite3_errstr(code).string
let details = sqlite3_errmsg(db).string
self.details = details == message ? nil : details
}
}

// MARK: - CustomNSError

extension DatabaseError: CustomNSError {
public static let errorDomain = "SQLyra.DatabaseErrorDomain"

public var errorCode: Int { Int(code) }

init(code: Int32, statement: PreparedStatement) {
self.init(code: code, db: statement.db)
public var errorUserInfo: [String: Any] {
var userInfo: [String: Any] = [:]
userInfo[NSLocalizedDescriptionKey] = message
userInfo[NSLocalizedFailureReasonErrorKey] = details
return userInfo
}
}

private init(code: Int32, db: OpaquePointer?) {
self.code = code
self.message = sqlite3_errstr(code).string ?? ""
self.reason = sqlite3_errmsg(db).string ?? ""
// MARK: - DatabaseHandle

protocol DatabaseHandle {
var db: OpaquePointer! { get }
}

extension DatabaseHandle {
@discardableResult
func check(_ code: Int32, _ success: Int32 = SQLITE_OK) throws -> Self {
guard code == success else {
throw DatabaseError(code: code, db: db)
}
return self
}
}
Loading

0 comments on commit e3f2447

Please sign in to comment.