diff --git a/Snippets/ErrorCodes.swift b/Snippets/ErrorCodes.swift new file mode 100644 index 0000000..12c8dd6 --- /dev/null +++ b/Snippets/ErrorCodes.swift @@ -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") diff --git a/Snippets/RetrievingStatementSQL.swift b/Snippets/RetrievingStatementSQL.swift index 461c24f..4fbb65e 100644 --- a/Snippets/RetrievingStatementSQL.swift +++ b/Snippets/RetrievingStatementSQL.swift @@ -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 diff --git a/Snippets/SQLParameters.swift b/Snippets/SQLParameters.swift index 6db3ca6..175a31e 100644 --- a/Snippets/SQLParameters.swift +++ b/Snippets/SQLParameters.swift @@ -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") diff --git a/Sources/SQLyra/Database.swift b/Sources/SQLyra/Database.swift index ef6d2c0..3122461 100644 --- a/Sources/SQLyra/Database.swift +++ b/Sources/SQLyra/Database.swift @@ -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 @@ -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. @@ -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: @@ -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:)``. @@ -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 + } } diff --git a/Sources/SQLyra/DatabaseError.swift b/Sources/SQLyra/DatabaseError.swift index 0b7967a..17082d7 100644 --- a/Sources/SQLyra/DatabaseError.swift +++ b/Sources/SQLyra/DatabaseError.swift @@ -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`. @@ -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) diff --git a/Sources/SQLyra/PreparedStatement.swift b/Sources/SQLyra/PreparedStatement.swift index 39fd8dc..a136d44 100644 --- a/Sources/SQLyra/PreparedStatement.swift +++ b/Sources/SQLyra/PreparedStatement.swift @@ -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 @@ -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 ?? "" } @@ -79,7 +79,7 @@ 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 ?? "" } @@ -87,7 +87,7 @@ extension PreparedStatement { /// 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 "" } @@ -101,13 +101,13 @@ 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) } @@ -115,7 +115,7 @@ extension PreparedStatement { /// 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)) diff --git a/Sources/SQLyra/SQLyra.docc/Database.md b/Sources/SQLyra/SQLyra.docc/Database.md index db28d85..d137443 100644 --- a/Sources/SQLyra/SQLyra.docc/Database.md +++ b/Sources/SQLyra/SQLyra.docc/Database.md @@ -10,3 +10,10 @@ - ``Database/execute(_:)`` - ``Database/prepare(_:)`` + +### Error Codes And Messages + +- ``Database/errorCode`` +- ``Database/extendedErrorCode`` +- ``Database/setExtendedResultCodesEnabled(_:)`` +- ``Database/errorMessage`` diff --git a/Tests/SQLyraTests/DatabaseTests.swift b/Tests/SQLyraTests/DatabaseTests.swift index 991e1fa..5cba083 100644 --- a/Tests/SQLyraTests/DatabaseTests.swift +++ b/Tests/SQLyraTests/DatabaseTests.swift @@ -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") + } + } }