Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Snippets/ErrorCodes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// snippet.hide
import SQLyra

let database = try Database.open(at: ":memory:", options: [.readwrite, .memory])
try database.execute("CREATE TABLE employees (id INT PRIMARY KEY NOT NULL, name TEXT);")

// snippet.show
database.setExtendedResultCodesEnabled(true) // or `Database.OpenOptions.extendedResultCode`

let errorCode = 19 // SQLITE_CONSTRAINT
let extendedErrorCode = 1299 // SQLITE_CONSTRAINT_NOTNULL

do {
try database.execute("INSERT INTO employees (name) VALUES ('John');")
} catch let error {
assert(error.code != errorCode)
assert(error.code == extendedErrorCode)
assert(error.codeDescription == "constraint failed")
assert(error.message == "NOT NULL constraint failed: employees.id")
}
assert(database.errorCode != errorCode)
assert(database.errorCode == extendedErrorCode)
assert(database.extendedErrorCode == extendedErrorCode)
assert(database.errorMessage == "NOT NULL constraint failed: employees.id")
2 changes: 1 addition & 1 deletion Snippets/RetrievingStatementSQL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ let statement = try db.prepare("INSERT INTO contacts (id, name) VALUES (?, ?);")
try statement.bind(parameters: 1, "Paul")

assert(statement.sql == "INSERT INTO contacts (id, name) VALUES (?, ?);")
assert(statement.expandedSQL == "INSERT INTO contacts (id, name) VALUES (1, 'Paul');")
assert(statement.normalizedSQL == "INSERT INTO contacts(id,name)VALUES(?,?);")
assert(statement.expandedSQL == "INSERT INTO contacts (id, name) VALUES (1, 'Paul');")

// snippet.hide
#endif
5 changes: 3 additions & 2 deletions Snippets/SQLParameters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ let db = try Database.open(at: ":memory:", options: [.memory, .readwrite])
try db.execute("CREATE TABLE users (id INT, email TEXT);")

// snippet.show
let statement = try db.prepare("INSERT INTO users (id, email) VALUES (?, :login)")

let statement = try db.prepare(
"INSERT INTO users (id, email) VALUES (?, :login)"
)
assert(statement.parameterCount == 2)
assert(statement.parameterName(at: 1) == nil)
assert(statement.parameterName(at: 2) == ":login")
Expand Down
61 changes: 60 additions & 1 deletion Sources/SQLyra/Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import SQLite3
/// @Snippet(path: "SQLyra/Snippets/GettingStarted")
public final class Database {
/// Database open options.
///
/// SQLite C Interface [Flags For File Open Operations](https://www.sqlite.org/c3ref/c_open_autoproxy.html)
public struct OpenOptions: OptionSet, Sendable {
/// SQLite flags for opening a database connection.
public let rawValue: Int32
Expand Down Expand Up @@ -36,6 +38,8 @@ public final class Database {
public static let memory = OpenOptions(rawValue: SQLITE_OPEN_MEMORY)

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

/// The filename can be interpreted as a URI if this flag is set.
Expand Down Expand Up @@ -93,6 +97,7 @@ public final class Database {

/// Opening a new database connection.
///
/// SQLite C Interface [Opening A New Database Connection](https://www.sqlite.org/c3ref/open.html).
/// - Parameters:
/// - 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:
Expand All @@ -102,7 +107,8 @@ public final class Database {
public static func open(at filename: String, options: OpenOptions = []) throws(DatabaseError) -> Database {
let database = Database()
let code = sqlite3_open_v2(filename, &database.db, options.rawValue, nil)
return try database.check(code)
try database.check(code)
return database
}

/// Use ``Database/open(at:options:)``.
Expand Down Expand Up @@ -137,4 +143,57 @@ public final class Database {
try check(sqlite3_prepare_v2(db, sql, -1, &stmt, nil))
return PreparedStatement(stmt: stmt, database: self)
}

// MARK: - Error Codes And Messages

/// If the most recent `sqlite3_*` API call associated with database connection failed,
/// then the property returns the numeric result code or extended result code for that API call.
///
/// SQLite C Interface
/// - [Result and Error Codes](https://www.sqlite.org/rescode.html)
/// - [Error Codes And Messages](https://www.sqlite.org/c3ref/errcode.html)
/// @Snippet(path: "SQLyra/Snippets/ErrorCodes")
public var errorCode: Int32 { sqlite3_errcode(db) }

/// Returns the extended result code even when extended result codes are disabled.
///
/// SQLite C Interface
/// - [Result and Error Codes](https://www.sqlite.org/rescode.html)
/// - [Error Codes And Messages](https://www.sqlite.org/c3ref/errcode.html)
/// @Snippet(path: "SQLyra/Snippets/ErrorCodes")
public var extendedErrorCode: Int32 { sqlite3_extended_errcode(db) }

/// Enable or disable extended result codes.
///
/// The extended result codes are disabled by default for historical compatibility.
///
/// SQLite C Interface
/// - [Result and Error Codes](https://www.sqlite.org/rescode.html)
/// - [Error Codes And Messages](https://www.sqlite.org/c3ref/errcode.html)
/// - [Enable Or Disable Extended Result Codes](https://www.sqlite.org/c3ref/extended_result_codes.html)
/// @Snippet(path: "SQLyra/Snippets/ErrorCodes")
public func setExtendedResultCodesEnabled(_ onOff: Bool) {
sqlite3_extended_result_codes(db, onOff ? 1 : 0)
}

/// Return English-language text that describes the error, or NULL if no error message is available.
///
/// The error string might be overwritten by subsequent calls to other SQLite interface functions.
///
/// SQLite C Interface
/// - [Result and Error Codes](https://www.sqlite.org/rescode.html)
/// - [Error Codes And Messages](https://www.sqlite.org/c3ref/errcode.html)
/// @Snippet(path: "SQLyra/Snippets/ErrorCodes")
public var errorMessage: String? { sqlite3_errmsg(db).string }

func error(code: Int32) -> DatabaseError {
DatabaseError(code: code, message: errorMessage)
}

func check(_ code: Int32, _ success: Int32 = SQLITE_OK) throws(DatabaseError) {
guard code == success else {
throw error(code: code)
}
// success
}
}
21 changes: 5 additions & 16 deletions Sources/SQLyra/DatabaseError.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import SQLite3

/// SQLite database error.
///
/// [Result and Error Codes](https://www.sqlite.org/rescode.html)
/// @Snippet(path: "SQLyra/Snippets/ErrorCodes")
public struct DatabaseError: Error, Equatable, Hashable {
/// Failed result code.
///
/// [Result and Error Codes](https://www.sqlite.org/rescode.html)
public let code: Int32

/// The English-language text that describes the result `code`, as UTF-8, or `nil`.
Expand All @@ -22,22 +27,6 @@ public struct DatabaseError: Error, Equatable, Hashable {
}
}

extension Database {
private var errorMessage: String? { sqlite3_errmsg(db).string }

func error(code: Int32) -> DatabaseError {
DatabaseError(code: code, message: errorMessage)
}

@discardableResult
func check(_ code: Int32, _ success: Int32 = SQLITE_OK) throws(DatabaseError) -> Database {
guard code == success else {
throw error(code: code)
}
return self
}
}

// MARK: - DatabaseError + Foundation

#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
Expand Down
14 changes: 7 additions & 7 deletions Sources/SQLyra/PreparedStatement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import FoundationEssentials
/// 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.
///
/// [Prepared Statement Object](https://www.sqlite.org/c3ref/stmt.html)
/// SQLite C Interface [Prepared Statement Object](https://www.sqlite.org/c3ref/stmt.html)
public final class PreparedStatement {
let stmt: OpaquePointer
let database: Database // release database after all statements
Expand Down Expand Up @@ -69,7 +69,7 @@ public final class PreparedStatement {
extension PreparedStatement {
/// SQL text used to create prepared statement.
///
/// [Retrieving statement SQL](https://www.sqlite.org/c3ref/expanded_sql.html)
/// SQLite C Interface [Retrieving statement SQL](https://www.sqlite.org/c3ref/expanded_sql.html)
/// @Snippet(path: "SQLyra/Snippets/RetrievingStatementSQL")
public var sql: String { sqlite3_sql(stmt).string ?? "" }

Expand All @@ -79,15 +79,15 @@ extension PreparedStatement {
/// The semantics used to normalize a SQL statement are unspecified and subject to change.
/// At a minimum, literal values will be replaced with suitable placeholders.
///
/// [Retrieving statement SQL](https://www.sqlite.org/c3ref/expanded_sql.html)
/// SQLite C Interface [Retrieving statement SQL](https://www.sqlite.org/c3ref/expanded_sql.html)
/// @Snippet(path: "SQLyra/Snippets/RetrievingStatementSQL")
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
public var normalizedSQL: String { sqlite3_normalized_sql(stmt).string ?? "" }
#endif

/// SQL text of prepared statement with bound parameters expanded.
///
/// [Retrieving statement SQL](https://www.sqlite.org/c3ref/expanded_sql.html)
/// SQLite C Interface [Retrieving statement SQL](https://www.sqlite.org/c3ref/expanded_sql.html)
/// @Snippet(path: "SQLyra/Snippets/RetrievingStatementSQL")
public var expandedSQL: String {
guard let pointer = sqlite3_expanded_sql(stmt) else { return "" }
Expand All @@ -101,21 +101,21 @@ extension PreparedStatement {
extension PreparedStatement {
/// Number of SQL parameters.
///
/// [Number Of SQL Parameters](https://www.sqlite.org/c3ref/bind_parameter_count.html).
/// SQLite C Interface [Number Of SQL Parameters](https://www.sqlite.org/c3ref/bind_parameter_count.html).
/// @Snippet(path: "SQLyra/Snippets/SQLParameters")
public var parameterCount: Int { Int(sqlite3_bind_parameter_count(stmt)) }

/// Name of a SQL parameter.
///
/// [Name of a SQL parameter](https://www.sqlite.org/c3ref/bind_parameter_name.html).
/// SQLite C Interface [Name of a SQL parameter](https://www.sqlite.org/c3ref/bind_parameter_name.html).
/// @Snippet(path: "SQLyra/Snippets/SQLParameters")
public func parameterName(at index: Int) -> String? {
sqlite3_bind_parameter_name(stmt, Int32(index)).map { String(cString: $0) }
}

/// Index of a parameter with a given name
///
/// [Index of a parameter with a given name](https://www.sqlite.org/c3ref/bind_parameter_index.html).
/// SQLite C Interface [Index of a parameter with a given name](https://www.sqlite.org/c3ref/bind_parameter_index.html).
/// @Snippet(path: "SQLyra/Snippets/SQLParameters")
public func parameterIndex(for name: String) -> Int {
Int(sqlite3_bind_parameter_index(stmt, name))
Expand Down
7 changes: 7 additions & 0 deletions Sources/SQLyra/SQLyra.docc/Database.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@

- ``Database/execute(_:)``
- ``Database/prepare(_:)``

### Error Codes And Messages

- ``Database/errorCode``
- ``Database/extendedErrorCode``
- ``Database/setExtendedResultCodesEnabled(_:)``
- ``Database/errorMessage``
40 changes: 40 additions & 0 deletions Tests/SQLyraTests/DatabaseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,44 @@ struct DatabaseTests {
]
#expect(contacts == expected)
}

struct ErrorCodes {

@Test func defaults() throws {
let database = try Database.open(at: ":memory:", options: [.readwrite, .memory])
try expect(errorCode: SQLITE_CONSTRAINT, database)
}

@Test func extendedResultCodeOption() throws {
let database = try Database.open(at: ":memory:", options: [.readwrite, .memory, .extendedResultCode])
try expect(errorCode: 1299, database)
}

@Test func setExtendedResultCodesEnabled() throws {
let database = try Database.open(at: ":memory:", options: [.readwrite, .memory])
database.setExtendedResultCodesEnabled(true)
try expect(errorCode: 1299, database)
}

@Test func setExtendedResultCodesDisabled() throws {
let database = try Database.open(at: ":memory:", options: [.readwrite, .memory, .extendedResultCode])
database.setExtendedResultCodesEnabled(false)
try expect(errorCode: SQLITE_CONSTRAINT, database)
}

private func expect(errorCode: Int32, _ database: Database) throws {
try database.execute("CREATE TABLE employees (id INT PRIMARY KEY NOT NULL, name TEXT);")

let error = #expect(throws: DatabaseError.self) {
try database.execute("INSERT INTO employees (name) VALUES ('John');")
}
#expect(error?.code == errorCode)
#expect(error?.codeDescription == "constraint failed")
#expect(error?.message == "NOT NULL constraint failed: employees.id")

#expect(database.errorCode == errorCode)
#expect(database.extendedErrorCode == 1299) // SQLITE_CONSTRAINT_NOTNULL
#expect(database.errorMessage == "NOT NULL constraint failed: employees.id")
}
}
}