Skip to content

Commit e3f2447

Browse files
Merge pull request #8 from Alexander-Ignition/big-refactor
Big refactor
2 parents 574a04c + 903424a commit e3f2447

File tree

13 files changed

+697
-496
lines changed

13 files changed

+697
-496
lines changed

.swift-format

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,6 @@
5454
"UseSynthesizedInitializer" : false,
5555
"UseTripleSlashForDocumentationComments" : true,
5656
"UseWhereClausesInForLoops" : false,
57-
"ValidateDocumentationComments" : false
57+
"ValidateDocumentationComments" : true
5858
}
5959
}

Makefile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,19 @@ $(OUTPUD_DIR)/Docs: $(DOCC_ARCHIVE)
5353
xcrun docc process-archive transform-for-static-hosting $^ \
5454
--hosting-base-path $(TARGET_NAME) \
5555
--output-path $@
56+
57+
# MARK: - DocC preview
58+
59+
DOC_CATALOG = Sources/$(TARGET_NAME)/$(TARGET_NAME).docc
60+
SYMBOL_GRAPHS = $(OUTPUD_DIR)/symbol-graphs
61+
62+
$(SYMBOL_GRAPHS):
63+
swift build --target $(TARGET_NAME) -Xswiftc -emit-symbol-graph -Xswiftc -emit-symbol-graph-dir -Xswiftc $@
64+
65+
$(OUTPUD_DIR)/doc-preview: $(DOC_CATALOG) $(SYMBOL_GRAPHS)
66+
xcrun docc preview $(DOC_CATALOG) \
67+
--fallback-display-name $(TARGET_NAME) \
68+
--fallback-bundle-identifier org.swift.$(TARGET_NAME) \
69+
--fallback-bundle-version 1.0.0 \
70+
--additional-symbol-graph-dir $(SYMBOL_GRAPHS) \
71+
--output-path $@

Sources/SQLyra/Database.swift

Lines changed: 96 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,147 +1,138 @@
11
import SQLite3
22

33
/// SQLite database.
4-
public final class Database {
5-
/// Execution callback type.
6-
///
7-
/// - SeeAlso: `Database.execute(_:handler:)`.
8-
public typealias ExecutionHandler = (_ row: [String: String]) -> Void
9-
10-
public struct OpenOptions: OptionSet {
4+
public final class Database: DatabaseHandle {
5+
/// Database open options.
6+
public struct OpenOptions: OptionSet, Sendable {
7+
/// SQLite flags for opening a database connection.
118
public let rawValue: Int32
129

1310
public init(rawValue: Int32) {
1411
self.rawValue = rawValue
1512
}
1613

17-
public init(_ rawValue: Int32) {
18-
self.rawValue = rawValue
19-
}
14+
// MARK: - Required
15+
16+
/// The database is created if it does not already exist.
17+
public static let create = OpenOptions(rawValue: SQLITE_OPEN_CREATE)
18+
19+
/// The database is opened for reading and writing if possible, or reading only if the file is write protected by the operating system.
20+
///
21+
/// In either case the database must already exist, otherwise an error is returned. For historical reasons,
22+
/// if opening in read-write mode fails due to OS-level permissions, an attempt is made to open it in read-only mode.
23+
public static let readwrite = OpenOptions(rawValue: SQLITE_OPEN_READWRITE)
24+
25+
/// The database is opened in read-only mode. If the database does not already exist, an error is returned.
26+
public static let readonly = OpenOptions(rawValue: SQLITE_OPEN_READONLY)
27+
28+
// MARK: - Addition
29+
30+
/// The database will be opened as an in-memory database.
31+
///
32+
/// The database is named by the "filename" argument for the purposes of cache-sharing,
33+
/// if shared cache mode is enabled, but the "filename" is otherwise ignored.
34+
public static let memory = OpenOptions(rawValue: SQLITE_OPEN_MEMORY)
35+
36+
/// The database connection comes up in "extended result code mode".
37+
public static let extendedResultCode = OpenOptions(rawValue: SQLITE_OPEN_EXRESCODE)
38+
39+
/// The filename can be interpreted as a URI if this flag is set.
40+
public static let uri = OpenOptions(rawValue: SQLITE_OPEN_URI)
41+
42+
/// The database filename is not allowed to contain a symbolic link.
43+
public static let noFollow = OpenOptions(rawValue: SQLITE_OPEN_NOFOLLOW)
2044

21-
public static var readonly: OpenOptions { .init(SQLITE_OPEN_READONLY) }
22-
public static var readwrite: OpenOptions { .init(SQLITE_OPEN_READWRITE) }
23-
public static var create: OpenOptions { .init(SQLITE_OPEN_CREATE) }
24-
public static var uri: OpenOptions { .init(SQLITE_OPEN_URI) }
25-
public static var memory: OpenOptions { .init(SQLITE_OPEN_MEMORY) }
26-
public static var noMutex: OpenOptions { .init(SQLITE_OPEN_NOMUTEX) }
27-
public static var fullMutex: OpenOptions { .init(SQLITE_OPEN_FULLMUTEX) }
28-
public static var sharedCache: OpenOptions { .init(SQLITE_OPEN_SHAREDCACHE) }
29-
public static var privateCache: OpenOptions { .init(SQLITE_OPEN_PRIVATECACHE) }
45+
// MARK: - Threading modes
46+
47+
/// The new database connection will use the "multi-thread" threading mode.
48+
///
49+
/// 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.
50+
///
51+
/// [Using SQLite in multi-threaded Applications](https://www.sqlite.org/threadsafe.html)
52+
public static let noMutex = OpenOptions(rawValue: SQLITE_OPEN_NOMUTEX)
53+
54+
/// The new database connection will use the "serialized" threading mode.
55+
///
56+
/// This means the multiple threads can safely attempt to use the same database connection at the same time.
57+
/// (Mutexes will block any actual concurrency, but in this mode there is no harm in trying.)
58+
///
59+
/// [Using SQLite in multi-threaded Applications](https://www.sqlite.org/threadsafe.html)
60+
public static let fullMutex = OpenOptions(rawValue: SQLITE_OPEN_FULLMUTEX)
61+
62+
// MARK: - Cache modes
63+
64+
/// The database is opened shared cache enabled.
65+
///
66+
/// - Warning: The use of shared cache mode is discouraged and hence shared cache capabilities may be omitted
67+
/// from many builds of SQLite. In such cases, this option is a no-op.
68+
///
69+
/// [SQLite Shared-Cache mode](https://www.sqlite.org/sharedcache.html)
70+
public static let sharedCache = OpenOptions(rawValue: SQLITE_OPEN_SHAREDCACHE)
71+
72+
/// The database is opened shared cache disabled.
73+
///
74+
/// [SQLite Shared-Cache mode](https://www.sqlite.org/sharedcache.html)
75+
public static let privateCache = OpenOptions(rawValue: SQLITE_OPEN_PRIVATECACHE)
3076
}
3177

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

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

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

4392
/// Opening a new database connection.
4493
///
4594
/// - Parameters:
46-
/// - path: Relative or absolute path to the database file.
47-
/// - options: Database open options.
95+
/// - filename: Relative or absolute path to the database file.
96+
/// - options: The options parameter must include, at a minimum, one of the following three option combinations:
97+
/// ``Database/OpenOptions/readonly``, ``Database/OpenOptions/readwrite``, ``Database/OpenOptions/create``.
4898
/// - Returns: A new database connection.
49-
/// - Throws: `DatabaseError`.
50-
public static func open(at path: String, options: OpenOptions = []) throws -> Database {
99+
/// - Throws: ``DatabaseError``
100+
public static func open(at filename: String, options: OpenOptions = []) throws -> Database {
51101
let database = Database()
52-
53-
let code = sqlite3_open_v2(path, &database.db, options.rawValue, nil)
54-
try database.check(code)
55-
56-
return database
102+
let code = sqlite3_open_v2(filename, &database.db, options.rawValue, nil)
103+
return try database.check(code)
57104
}
58105

59-
/// Use `Database.open(at:options:)`.
106+
/// Use ``Database/open(at:options:)``.
60107
private init() {}
61108

62109
deinit {
63110
let code = sqlite3_close_v2(db)
64111
assert(code == SQLITE_OK, "sqlite3_close_v2(): \(code)")
65112
}
66113

67-
/// Run multiple statements of SQL.
114+
/// One-step query execution Interface.
68115
///
69-
/// - Parameter sql: statements.
70-
/// - Throws: `DatabaseError`.
71-
public func execute(_ sql: String) throws {
72-
let status = sqlite3_exec(db, sql, nil, nil, nil)
73-
try check(status)
74-
}
75-
76-
/// Run multiple statements of SQL with row handler.
116+
/// The convenience wrapper around ``Database/prepare(_:)`` and ``PreparedStatement``,
117+
/// that allows an application to run multiple statements of SQL without having to use a lot code.
77118
///
78-
/// - Parameters:
79-
/// - sql: statements.
80-
/// - handler: Table row handler.
81-
/// - Throws: `DatabaseError`.
82-
public func execute(_ sql: String, handler: @escaping ExecutionHandler) throws {
83-
let context = ExecutionContext(handler)
84-
let ctx = Unmanaged.passUnretained(context).toOpaque()
85-
let status = sqlite3_exec(db, sql, readRow, ctx, nil)
86-
try check(status)
87-
}
88-
89-
/// Compiling an SQL statement.
90-
public func prepare(_ sql: String, _ parameters: SQLParameter?...) throws -> PreparedStatement {
91-
try prepare(sql, parameters: parameters)
119+
/// - Parameter sql: UTF-8 encoded, semicolon-separate SQL statements to be evaluated.
120+
/// - Throws: ``DatabaseError``
121+
public func execute(_ sql: String) throws {
122+
try check(sqlite3_exec(db, sql, nil, nil, nil))
92123
}
93124

94125
/// Compiling an SQL statement.
95-
public func prepare(_ sql: String, parameters: [SQLParameter?]) throws -> PreparedStatement {
96-
var stmt: OpaquePointer!
97-
let code = sqlite3_prepare_v2(db, sql, -1, &stmt, nil)
98-
try check(code)
99-
let statement = PreparedStatement(stmt: stmt)
100-
try statement.bind(parameters: parameters)
101-
return statement
102-
}
103-
104-
/// Check result code.
105126
///
106-
/// - Throws: `DatabaseError` if code not ok.
107-
private func check(_ code: Int32) throws {
108-
if code != SQLITE_OK {
109-
throw DatabaseError(code: code, database: self)
110-
}
111-
}
112-
113-
}
114-
115-
private final class ExecutionContext {
116-
let handler: Database.ExecutionHandler
117-
118-
init(_ handler: @escaping Database.ExecutionHandler) {
119-
self.handler = handler
120-
}
121-
}
122-
123-
private func readRow(
124-
ctx: UnsafeMutableRawPointer?,
125-
argc: Int32,
126-
argv: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?,
127-
columns: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
128-
) -> Int32 {
129-
guard let ctx, let argv, let columns else {
130-
return SQLITE_OK
131-
}
132-
let count = Int(argc)
133-
var row = [String: String](minimumCapacity: count)
134-
135-
for index in 0..<count {
136-
guard let ptr = columns.advanced(by: index).pointee else {
137-
continue
138-
}
139-
let name = String(cString: ptr)
140-
if let value = argv.advanced(by: index).pointee {
141-
row[name] = String(cString: value)
142-
}
127+
/// To execute an SQL statement, it must first be compiled into a byte-code program using one of these routines.
128+
/// Or, in other words, these routines are constructors for the prepared statement object.
129+
///
130+
/// - Parameter sql: The statement to be compiled, encoded as UTF-8.
131+
/// - Returns: A compiled prepared statement that can be executed.
132+
/// - Throws: ``DatabaseError``
133+
public func prepare(_ sql: String) throws -> PreparedStatement {
134+
var stmt: OpaquePointer!
135+
try check(sqlite3_prepare_v2(db, sql, -1, &stmt, nil))
136+
return PreparedStatement(stmt: stmt)
143137
}
144-
let context = Unmanaged<ExecutionContext>.fromOpaque(ctx).takeUnretainedValue()
145-
context.handler(row)
146-
return SQLITE_OK
147138
}

Sources/SQLyra/DatabaseError.swift

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Foundation
12
import SQLite3
23

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

89
/// A short error description.
9-
public var message: String
10+
public var message: String?
1011

1112
/// A complete sentence (or more) describing why the operation failed.
12-
public let reason: String
13+
public let details: String?
1314

1415
/// A new database error.
1516
///
1617
/// - Parameters:
1718
/// - code: failed result code.
1819
/// - message: A short error description.
19-
/// - reason: A complete sentence (or more) describing why the operation failed.
20-
public init(code: Int32, message: String, reason: String) {
20+
/// - details: A complete sentence (or more) describing why the operation failed.
21+
public init(code: Int32, message: String, details: String) {
2122
self.code = code
2223
self.message = message
23-
self.reason = reason
24+
self.details = details
2425
}
2526

26-
init(code: Int32, database: Database) {
27-
self.init(code: code, db: database.db)
27+
init(code: Int32, db: OpaquePointer?) {
28+
self.code = code
29+
self.message = sqlite3_errstr(code).string
30+
let details = sqlite3_errmsg(db).string
31+
self.details = details == message ? nil : details
2832
}
33+
}
34+
35+
// MARK: - CustomNSError
36+
37+
extension DatabaseError: CustomNSError {
38+
public static let errorDomain = "SQLyra.DatabaseErrorDomain"
39+
40+
public var errorCode: Int { Int(code) }
2941

30-
init(code: Int32, statement: PreparedStatement) {
31-
self.init(code: code, db: statement.db)
42+
public var errorUserInfo: [String: Any] {
43+
var userInfo: [String: Any] = [:]
44+
userInfo[NSLocalizedDescriptionKey] = message
45+
userInfo[NSLocalizedFailureReasonErrorKey] = details
46+
return userInfo
3247
}
48+
}
3349

34-
private init(code: Int32, db: OpaquePointer?) {
35-
self.code = code
36-
self.message = sqlite3_errstr(code).string ?? ""
37-
self.reason = sqlite3_errmsg(db).string ?? ""
50+
// MARK: - DatabaseHandle
51+
52+
protocol DatabaseHandle {
53+
var db: OpaquePointer! { get }
54+
}
55+
56+
extension DatabaseHandle {
57+
@discardableResult
58+
func check(_ code: Int32, _ success: Int32 = SQLITE_OK) throws -> Self {
59+
guard code == success else {
60+
throw DatabaseError(code: code, db: db)
61+
}
62+
return self
3863
}
3964
}

0 commit comments

Comments
 (0)