diff --git a/AGENTS.md b/AGENTS.md index a63d25a..d75ab44 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,13 @@ ## Testing direction - Prefer Swift Testing over XCTest for new or updated tests. Existing XCTest cases are legacy and should be migrated opportunistically. Keep tests runnable across Apple and non-Apple platforms. +- Follow the Swift Testing style used in the blob tests: group by feature under `Tests/LSQLiteTests`, use `@Suite("...")`, and give `@Test` cases descriptive names. +- Test files mirror source files with a `Tests` suffix, and each test file defines exactly one `@Suite` named after the original file (without the `Tests` suffix). +- Use in-memory databases for isolation, set up shared fixtures in `init()` with `#require` on result codes and optional unwrapping, and tear down with `deinit` or `defer` when a handle must be closed. +- Assert SQLite semantics via `ResultCode` with `#expect` (for example, `.ok`, `.error`, `.busy`, `.abort`) instead of introducing throwing flows. +- Keep tests focused and readable by extracting repeated buffer or pointer helpers into file-private functions. +- Keep test control flow linear with no branching; use `try #require` to unwrap optionals or verify prerequisites before continuing. +- Focus tests on validating the wrapper behavior and surface (rawValue round trips, ResultCode mapping, handle lifecycle), not SQLite's own functionality. ## Platform expectations - Non-Apple platforms are fully supported. Gate Apple-only constants and behaviors with the appropriate `canImport` checks, and rely on the `MissedSwiftSQLite` target to expose any SQLite constants or helpers that the Swift importer misses on Linux. diff --git a/Package.swift b/Package.swift index 14a42c0..fd8ca87 100644 --- a/Package.swift +++ b/Package.swift @@ -26,7 +26,7 @@ let package = Package( ), .testTarget( name: "LSQLiteTests", - dependencies: ["LSQLite"] + dependencies: ["LSQLite", "MissedSwiftSQLite"] ), ] ) diff --git a/Sources/LSQLite/Blob/Blob+Access.swift b/Sources/LSQLite/Blob/Blob+Access.swift index ae4ebe7..3adb169 100644 --- a/Sources/LSQLite/Blob/Blob+Access.swift +++ b/Sources/LSQLite/Blob/Blob+Access.swift @@ -3,31 +3,38 @@ import MissedSwiftSQLite extension Blob { /// Size of this open BLOB in bytes. /// - /// Related SQLite: `sqlite3_blob_bytes`, `sqlite3_blob_open`, `sqlite3_blob_close` + /// Returns 0 if the handle is aborted. + /// + /// Related SQLite: `sqlite3_blob_bytes` @inlinable public var byteCount: Int32 { sqlite3_blob_bytes(rawValue) } /// Reads `length` bytes from the BLOB starting at `offset` into `buffer`. + /// + /// Use `byteCount` to validate bounds before reading. /// - Parameters: /// - buffer: Destination buffer. /// - length: Number of bytes to copy. /// - offset: Byte offset within the BLOB. - /// - Returns: Result of `sqlite3_blob_read`. + /// - Returns: Result code for the read. /// - /// Related SQLite: `sqlite3_blob_read`, `sqlite3_blob_bytes`, `sqlite3_blob_open` + /// Related SQLite: `sqlite3_blob_read` @inlinable public func read(into buffer: UnsafeMutableRawPointer, length: Int32, offset: Int32) -> ResultCode { sqlite3_blob_read(rawValue, buffer, length, offset).resultCode } - /// Writes `length` bytes from `buffer` into the BLOB starting at `offset`; handle must be opened for writing. + /// Writes `length` bytes from `buffer` into the BLOB starting at `offset`. + /// + /// The handle must be opened for writing and the write does not change the BLOB size. + /// Use `byteCount` to validate bounds before writing. /// - Parameters: /// - buffer: Source bytes to write. /// - length: Number of bytes to write. /// - offset: Byte offset within the BLOB. - /// - Returns: Result of `sqlite3_blob_write`. + /// - Returns: Result code for the write. /// - /// Related SQLite: `sqlite3_blob_write`, `sqlite3_blob_bytes`, `sqlite3_blob_open` + /// Related SQLite: `sqlite3_blob_write` @inlinable public func write(_ buffer: UnsafeRawPointer, length: Int32, offset: Int32) -> ResultCode { sqlite3_blob_write(rawValue, buffer, length, offset).resultCode } diff --git a/Sources/LSQLite/Blob/Blob+Lifecycle.swift b/Sources/LSQLite/Blob/Blob+Lifecycle.swift index ca31fca..dadc474 100644 --- a/Sources/LSQLite/Blob/Blob+Lifecycle.swift +++ b/Sources/LSQLite/Blob/Blob+Lifecycle.swift @@ -1,19 +1,24 @@ import MissedSwiftSQLite extension Blob { - /// Moves this BLOB handle to a different row of the same table. + /// Moves this handle to a different row of the same table. + /// + /// The database, table, and column stay the same. On failure, the handle becomes + /// aborted and `read` or `write` return `.abort`. /// - Parameter rowID: Target rowid. - /// - Returns: Result of `sqlite3_blob_reopen`. + /// - Returns: Result code for the operation. /// - /// Related SQLite: `sqlite3_blob_reopen`, `sqlite3_blob_read`, `sqlite3_blob_write`, `sqlite3_blob_bytes` + /// Related SQLite: `sqlite3_blob_reopen` @inlinable public func reopen(at rowID: RowID) -> ResultCode { sqlite3_blob_reopen(rawValue, rowID.rawValue).resultCode } - /// Closes this BLOB handle; auto-commit transactions may finalize if no other writers remain. - /// - Returns: Result of `sqlite3_blob_close`. + /// Closes this handle. + /// + /// Closing always releases the handle, even if an error is returned. + /// - Returns: Result code for the close. /// - /// Related SQLite: `sqlite3_blob_close`, `sqlite3_errcode`, `sqlite3_errmsg` + /// Related SQLite: `sqlite3_blob_close` @inlinable public func close() -> ResultCode { sqlite3_blob_close(rawValue).resultCode } diff --git a/Sources/LSQLite/Blob/Blob.swift b/Sources/LSQLite/Blob/Blob.swift index e2fa8e1..d83edaa 100644 --- a/Sources/LSQLite/Blob/Blob.swift +++ b/Sources/LSQLite/Blob/Blob.swift @@ -1,6 +1,9 @@ -/// Wrapper around an open SQLite BLOB handle for incremental I/O. +/// Wrapper around an open BLOB handle for incremental I/O. /// -/// Related SQLite: `sqlite3_blob_open`, `sqlite3_blob_close`, `sqlite3_blob_read`, `sqlite3_blob_write`, `sqlite3_blob_bytes` +/// Create a handle with `Database.openBlob(_:databaseName:tableName:columnName:rowID:flags:)` +/// and close it when finished. Do not use the handle after closing. +/// +/// Related SQLite: `sqlite3_blob`, `sqlite3_blob_open`, `sqlite3_blob_close`, `sqlite3_blob_read`, `sqlite3_blob_write`, `sqlite3_blob_bytes` @frozen public struct Blob: RawRepresentable { public let rawValue: OpaquePointer diff --git a/Sources/LSQLite/Database/Database+FileNameURI.swift b/Sources/LSQLite/Database/Database+FileNameURI.swift deleted file mode 100644 index b5d83c2..0000000 --- a/Sources/LSQLite/Database/Database+FileNameURI.swift +++ /dev/null @@ -1,385 +0,0 @@ -extension Database.FileName { - /// Components used to build SQLite URI filenames. - /// - /// Related SQLite: "URI filenames", `sqlite3_open_v2`, `SQLITE_OPEN_URI` - public enum URI { - /// URI scheme component. - /// - /// Note: SQLite only interprets URIs that use the `"file"` scheme. - /// - /// Related SQLite: "URI filenames", `"file:"` - @frozen public struct Scheme: RawRepresentable { - public let rawValue: String - - @inlinable public init(rawValue: String) { - self.rawValue = rawValue - } - - /// SQLite file URI scheme. - /// - /// Related SQLite: `"file:"` - public static let file = Self(rawValue: "file") - } - - /// URI authority component. - /// - /// Related SQLite: "URI filenames (authority)" - @frozen public struct Authority: RawRepresentable { - public let rawValue: String - - @inlinable public init(rawValue: String) { - self.rawValue = rawValue - } - - /// Empty authority (`file:///path`). - /// - /// Related SQLite: "URI filenames (authority)" - public static let empty = Self(rawValue: "") - - /// `localhost` authority (`file://localhost/path`). - /// - /// Related SQLite: "URI filenames (authority)" - public static let localhost = Self(rawValue: "localhost") - } - - /// URI path component. - /// - /// Related SQLite: "URI filenames (path)", `sqlite3_open_v2` - @frozen public struct Path: RawRepresentable { - public let rawValue: String - - @inlinable public init(rawValue: String) { - self.rawValue = rawValue - } - - /// Creates a path component by escaping delimiters reserved by SQLite URI parsing. - /// - Parameter unescaped: Path text that will be percent-escaped for `?`, `#`, and `%`. - /// - /// Related SQLite: "URI filenames (path)", escape sequences (`%HH`) - @inlinable public init(_ unescaped: String) { - self.rawValue = URI.percentEncode(unescaped, escaping: { byte in - byte == UInt8(ascii: "?") || byte == UInt8(ascii: "#") || byte == UInt8(ascii: "%") - }) - } - - @inlinable func requiringLeadingSlash() -> Self { - guard !rawValue.isEmpty, !rawValue.hasPrefix("/") else { return self } - return Self(rawValue: "/" + rawValue) - } - } - - /// URI query component (without the leading `?`). - /// - /// Related SQLite: "URI filenames (query string)" - @frozen public struct Query: RawRepresentable { - public let rawValue: String - - @inlinable public init(rawValue: String) { - self.rawValue = rawValue - } - - /// Creates a query component from query parameters. - /// - /// Related SQLite: "URI filenames (query parameters)" - @inlinable public init(_ parameters: [QueryParameter]) { - var result = "" - result.reserveCapacity(parameters.count * 8) - for (index, parameter) in parameters.enumerated() { - if index != 0 { - result.append("&") - } - result.append(parameter.key.rawValue) - result.append("=") - result.append(parameter.value.rawValue) - } - self.rawValue = result - } - } - - /// URI fragment component (without the leading `#`). - /// - /// Note: SQLite ignores fragments in URI filenames. - /// - /// Related SQLite: "URI filenames (fragment)" - @frozen public struct Fragment: RawRepresentable { - public let rawValue: String - - @inlinable public init(rawValue: String) { - self.rawValue = rawValue - } - - /// Creates a fragment component by escaping `%`. - /// - /// Related SQLite: "URI filenames (fragment)", escape sequences (`%HH`) - @inlinable public init(_ unescaped: String) { - self.rawValue = URI.percentEncode(unescaped, escaping: { $0 == UInt8(ascii: "%") }) - } - } - - /// URI query parameter key. - /// - /// Related SQLite: "URI filenames (query parameters)" - @frozen public struct QueryKey: RawRepresentable { - public let rawValue: String - - @inlinable public init(rawValue: String) { - self.rawValue = rawValue - } - - /// Creates a key by escaping `%`, `&`, `=`, and `#`. - /// - /// Related SQLite: "URI filenames (query parameters)", escape sequences (`%HH`) - @inlinable public init(_ unescaped: String) { - self.rawValue = URI.percentEncode(unescaped, escaping: { byte in - byte == UInt8(ascii: "%") - || byte == UInt8(ascii: "&") - || byte == UInt8(ascii: "=") - || byte == UInt8(ascii: "#") - }) - } - } - - /// URI query parameter value. - /// - /// Related SQLite: "URI filenames (query parameters)" - @frozen public struct QueryValue: RawRepresentable { - public let rawValue: String - - @inlinable public init(rawValue: String) { - self.rawValue = rawValue - } - - /// Creates a value by escaping `%`, `&`, `=`, and `#`. - /// - /// Related SQLite: "URI filenames (query parameters)", escape sequences (`%HH`) - @inlinable public init(_ unescaped: String) { - self.rawValue = URI.percentEncode(unescaped, escaping: { byte in - byte == UInt8(ascii: "%") - || byte == UInt8(ascii: "&") - || byte == UInt8(ascii: "=") - || byte == UInt8(ascii: "#") - }) - } - } - - /// URI query parameter key/value pair. - /// - /// Related SQLite: "URI filenames (query parameters)" - @frozen public struct QueryParameter { - public let key: QueryKey - public let value: QueryValue - - @inlinable public init(key: QueryKey, value: QueryValue) { - self.key = key - self.value = value - } - - /// Creates a `cache=` query parameter. - /// - /// Related SQLite: `cache=shared`, `cache=private` - @inlinable public static func cache(_ cache: Cache) -> Self { - Self(key: QueryKey(rawValue: "cache"), value: QueryValue(rawValue: cache.rawValue)) - } - - /// Creates an `immutable=` query parameter. - /// - /// Related SQLite: `immutable=1`, `SQLITE_IOCAP_IMMUTABLE` - @inlinable public static func immutable(_ value: Boolean) -> Self { - Self(key: QueryKey(rawValue: "immutable"), value: QueryValue(rawValue: value.rawValue)) - } - - /// Creates a `mode=` query parameter. - /// - /// Related SQLite: `mode=ro`, `mode=rw`, `mode=rwc`, `mode=memory` - @inlinable public static func mode(_ mode: Mode) -> Self { - Self(key: QueryKey(rawValue: "mode"), value: QueryValue(rawValue: mode.rawValue)) - } - - /// Creates a `modeof=` query parameter. - /// - /// Related SQLite: `modeof=filename` - @inlinable public static func modeof(_ filename: QueryValue) -> Self { - Self(key: QueryKey(rawValue: "modeof"), value: filename) - } - - /// Creates a `nolock=` query parameter. - /// - /// Related SQLite: `nolock=1` - @inlinable public static func nolock(_ value: Boolean) -> Self { - Self(key: QueryKey(rawValue: "nolock"), value: QueryValue(rawValue: value.rawValue)) - } - - /// Creates a `psow=` query parameter. - /// - /// Related SQLite: `psow=0`, `psow=1` - @inlinable public static func psow(_ value: Boolean) -> Self { - Self(key: QueryKey(rawValue: "psow"), value: QueryValue(rawValue: value.rawValue)) - } - - /// Creates a `vfs=` query parameter. - /// - /// Related SQLite: `vfs=NAME`, `sqlite3_vfs_register` - @inlinable public static func vfs(_ vfs: VFS) -> Self { - Self(key: QueryKey(rawValue: "vfs"), value: QueryValue(rawValue: vfs.rawValue)) - } - } - - /// Value for the `mode=` query parameter. - /// - /// Related SQLite: `mode=ro`, `mode=rw`, `mode=rwc`, `mode=memory` - @frozen public struct Mode: RawRepresentable { - public let rawValue: String - - @inlinable public init(rawValue: String) { - self.rawValue = rawValue - } - - public static let ro = Self(rawValue: "ro") - public static let rw = Self(rawValue: "rw") - public static let rwc = Self(rawValue: "rwc") - public static let memory = Self(rawValue: "memory") - } - - /// Value for the `cache=` query parameter. - /// - /// Related SQLite: `cache=shared`, `cache=private` - @frozen public struct Cache: RawRepresentable { - public let rawValue: String - - @inlinable public init(rawValue: String) { - self.rawValue = rawValue - } - - public static let shared = Self(rawValue: "shared") - public static let `private` = Self(rawValue: "private") - } - - /// Boolean query parameter value wrapper. - /// - /// Related SQLite: boolean query parameters such as `immutable=1`, `nolock=1`, `psow=1` - @frozen public struct Boolean: RawRepresentable { - public let rawValue: String - - @inlinable public init(rawValue: String) { - self.rawValue = rawValue - } - - public static let enabled = Self(rawValue: "1") - public static let disabled = Self(rawValue: "0") - } - - /// Value for the `vfs=` query parameter. - /// - /// Related SQLite: `vfs=NAME`, `sqlite3_vfs_register` - @frozen public struct VFS: RawRepresentable { - public let rawValue: String - - @inlinable public init(rawValue: String) { - self.rawValue = rawValue - } - } - - @usableFromInline - static func percentEncode(_ string: String, escaping shouldEscape: (UInt8) -> Bool) -> String { - var needsEscaping = false - for byte in string.utf8 { - if shouldEscape(byte) { - needsEscaping = true - break - } - } - guard needsEscaping else { return string } - - var result = "" - result.reserveCapacity(string.utf8.count) - for scalar in string.unicodeScalars { - guard scalar.isASCII else { - result.unicodeScalars.append(scalar) - continue - } - - let byte = UInt8(scalar.value) - if shouldEscape(byte) { - result.append("%") - result.append(Self.hexDigitUppercase(byte >> 4)) - result.append(Self.hexDigitUppercase(byte & 0x0F)) - } else { - result.unicodeScalars.append(scalar) - } - } - return result - } - - @usableFromInline - static func hexDigitUppercase(_ nibble: UInt8) -> Character { - let nibble = nibble & 0x0F - let base: UInt32 = nibble < 10 ? 48 : 55 - let value = base + UInt32(nibble) - return Character(Unicode.Scalar(value)!) - } - } - - /// Creates a filename wrapper from URI components. - /// - /// The resulting string is suitable for passing into `sqlite3_open_v2` when URI processing is enabled. - /// - /// Related SQLite: "URI filenames", `sqlite3_open_v2`, `SQLITE_OPEN_URI` - @inlinable public static func uri( - scheme: URI.Scheme = .file, - path: URI.Path, - query: URI.Query? = nil, - fragment: URI.Fragment? = nil - ) -> Self { - Self._uri(scheme: scheme, authority: nil, path: path, query: query, fragment: fragment) - } - - /// Creates a filename wrapper from URI components. - /// - /// Related SQLite: "URI filenames", `sqlite3_open_v2`, `SQLITE_OPEN_URI` - @inlinable public static func uri( - scheme: URI.Scheme = .file, - authority: URI.Authority, - path: URI.Path? = nil, - query: URI.Query? = nil, - fragment: URI.Fragment? = nil - ) -> Self { - Self._uri(scheme: scheme, authority: authority, path: path, query: query, fragment: fragment) - } - - @usableFromInline - static func _uri( - scheme: URI.Scheme, - authority: URI.Authority?, - path: URI.Path?, - query: URI.Query?, - fragment: URI.Fragment? - ) -> Self { - var result = "" - result.reserveCapacity(32) - - result.append(scheme.rawValue) - result.append(":") - - if let authority { - result.append("//") - result.append(authority.rawValue) - if let path { - result.append(path.requiringLeadingSlash().rawValue) - } - } else if let path { - result.append(path.rawValue) - } - - if let query { - result.append("?") - result.append(query.rawValue) - } - - if let fragment { - result.append("#") - result.append(fragment.rawValue) - } - - return Self(rawValue: result) - } -} diff --git a/Tests/LSQLiteTests/Blob/Blob+AccessTests.swift b/Tests/LSQLiteTests/Blob/Blob+AccessTests.swift new file mode 100644 index 0000000..53b006d --- /dev/null +++ b/Tests/LSQLiteTests/Blob/Blob+AccessTests.swift @@ -0,0 +1,102 @@ +import LSQLite +import Testing + +@Suite("Blob+Access") +final class BlobAccessTests { + private let database: Database + private let rowID: RowID + private let missingRowID: RowID + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + self.database = openDatabase + + try #require(openDatabase.exec("CREATE TABLE blobs(data BLOB NOT NULL)") == .ok) + try #require(openDatabase.exec("INSERT INTO blobs(data) VALUES (zeroblob(4))") == .ok) + rowID = openDatabase.lastInsertedRowID() + missingRowID = RowID(rawValue: rowID.rawValue + 1) + } + + deinit { + _ = database.close() + } + + @Test("read/write and byteCount for open blob") + func readWriteAndByteCount() throws { + var blob: Blob? + let openResult = database.openBlob(&blob, databaseName: "main", tableName: "blobs", columnName: "data", rowID: rowID, flags: .readwrite) + #expect(openResult == .ok) + let openedBlob = try #require(blob) + defer { _ = openedBlob.close() } + + #expect(openedBlob.byteCount == 4) + + let writeResult = try writeBytes([1, 2, 3, 4], to: openedBlob, offset: 0) + #expect(writeResult == .ok) + + let (finalReadResult, finalBytes) = try readBytes(from: openedBlob, count: 4, offset: 0) + #expect(finalReadResult == .ok) + #expect(finalBytes == [1, 2, 3, 4]) + } + + @Test("read returns error when out of range") + func readOutOfRangeReturnsError() throws { + var blob: Blob? + let openResult = database.openBlob(&blob, databaseName: "main", tableName: "blobs", columnName: "data", rowID: rowID, flags: .readonly) + #expect(openResult == .ok) + let openedBlob = try #require(blob) + defer { _ = openedBlob.close() } + + let (readResult, _) = try readBytes(from: openedBlob, count: 5, offset: 0) + #expect(readResult == .error) + } + + @Test("write on readonly blob returns readonly") + func writeOnReadonlyBlobReturnsReadonly() throws { + var blob: Blob? + let openResult = database.openBlob(&blob, databaseName: "main", tableName: "blobs", columnName: "data", rowID: rowID, flags: .readonly) + #expect(openResult == .ok) + let openedBlob = try #require(blob) + defer { _ = openedBlob.close() } + + let writeResult = try writeBytes([0xFF], to: openedBlob, offset: 0) + #expect(writeResult == .readonly) + } + + @Test("read/write on aborted blob returns abort") + func readWriteOnAbortedBlobReturnsAbort() throws { + var blob: Blob? + let openResult = database.openBlob(&blob, databaseName: "main", tableName: "blobs", columnName: "data", rowID: rowID, flags: .readwrite) + #expect(openResult == .ok) + let openedBlob = try #require(blob) + defer { _ = openedBlob.close() } + + let reopenResult = openedBlob.reopen(at: missingRowID) + #expect(reopenResult == .error) + #expect(openedBlob.byteCount == 0) + + let (readResult, _) = try readBytes(from: openedBlob, count: 1, offset: 0) + #expect(readResult == .abort) + + let writeResult = try writeBytes([0xFF], to: openedBlob, offset: 0) + #expect(writeResult == .abort) + } +} + +private func readBytes(from blob: Blob, count: Int32, offset: Int32) throws -> (ResultCode, [UInt8]) { + var buffer = [UInt8](repeating: 0, count: Int(count)) + let result = try buffer.withUnsafeMutableBytes { bytes -> ResultCode in + let baseAddress = try #require(bytes.baseAddress) + return blob.read(into: baseAddress, length: count, offset: offset) + } + return (result, buffer) +} + +private func writeBytes(_ bytes: [UInt8], to blob: Blob, offset: Int32) throws -> ResultCode { + try bytes.withUnsafeBytes { buffer -> ResultCode in + let baseAddress = try #require(buffer.baseAddress) + return blob.write(baseAddress, length: Int32(buffer.count), offset: offset) + } +} diff --git a/Tests/LSQLiteTests/Blob/Blob+LifecycleTests.swift b/Tests/LSQLiteTests/Blob/Blob+LifecycleTests.swift new file mode 100644 index 0000000..01c031d --- /dev/null +++ b/Tests/LSQLiteTests/Blob/Blob+LifecycleTests.swift @@ -0,0 +1,105 @@ +import LSQLite +import Testing + +@Suite("Blob+Lifecycle") +final class BlobLifecycleTests { + private let database: Database + private let firstRowID: RowID + private let secondRowID: RowID + private let missingRowID: RowID + private var didCloseDatabase = false + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + self.database = openDatabase + + try #require(openDatabase.exec("CREATE TABLE blobs(data BLOB NOT NULL)") == .ok) + try #require(openDatabase.exec("INSERT INTO blobs(data) VALUES (x'010203')") == .ok) + firstRowID = openDatabase.lastInsertedRowID() + + try #require(openDatabase.exec("INSERT INTO blobs(data) VALUES (x'0A0B0C')") == .ok) + secondRowID = openDatabase.lastInsertedRowID() + missingRowID = RowID(rawValue: secondRowID.rawValue + 1) + } + + deinit { + guard !didCloseDatabase else { return } + _ = database.close() + } + + @Test("reopen moves handle to another row") + func reopenMovesHandleToAnotherRow() throws { + var blob: Blob? + let openResult = database.openBlob(&blob, databaseName: "main", tableName: "blobs", columnName: "data", rowID: firstRowID, flags: .readonly) + #expect(openResult == .ok) + let openedBlob = try #require(blob) + defer { _ = openedBlob.close() } + + let (firstReadResult, firstBytes) = try readBytes(from: openedBlob, count: 3, offset: 0) + #expect(firstReadResult == .ok) + #expect(firstBytes == [0x01, 0x02, 0x03]) + + let reopenResult = openedBlob.reopen(at: secondRowID) + #expect(reopenResult == .ok) + + let (secondReadResult, secondBytes) = try readBytes(from: openedBlob, count: 3, offset: 0) + #expect(secondReadResult == .ok) + #expect(secondBytes == [0x0A, 0x0B, 0x0C]) + } + + @Test("reopen to missing row aborts handle") + func reopenMissingRowAbortsHandle() throws { + var blob: Blob? + let openResult = database.openBlob(&blob, databaseName: "main", tableName: "blobs", columnName: "data", rowID: secondRowID, flags: .readwrite) + #expect(openResult == .ok) + let openedBlob = try #require(blob) + defer { _ = openedBlob.close() } + + let reopenResult = openedBlob.reopen(at: missingRowID) + #expect(reopenResult == .error) + #expect(openedBlob.byteCount == 0) + + let (readResult, _) = try readBytes(from: openedBlob, count: 1, offset: 0) + #expect(readResult == .abort) + + let writeResult = try writeBytes([0xFF], to: openedBlob, offset: 0) + #expect(writeResult == .abort) + + let reopenAgainResult = openedBlob.reopen(at: secondRowID) + #expect(reopenAgainResult == .abort) + } + + @Test("database close fails while blob is open") + func databaseCloseFailsWhileBlobIsOpen() throws { + var blob: Blob? + let openResult = database.openBlob(&blob, databaseName: "main", tableName: "blobs", columnName: "data", rowID: firstRowID, flags: .readonly) + #expect(openResult == .ok) + let openedBlob = try #require(blob) + + let closeWhileOpen = database.close() + #expect(closeWhileOpen == .busy) + + _ = openedBlob.close() + let closeAfterBlob = database.close() + #expect(closeAfterBlob == .ok) + didCloseDatabase = closeAfterBlob == .ok + } +} + +private func readBytes(from blob: Blob, count: Int32, offset: Int32) throws -> (ResultCode, [UInt8]) { + var buffer = [UInt8](repeating: 0, count: Int(count)) + let result = try buffer.withUnsafeMutableBytes { bytes -> ResultCode in + let baseAddress = try #require(bytes.baseAddress) + return blob.read(into: baseAddress, length: count, offset: offset) + } + return (result, buffer) +} + +private func writeBytes(_ bytes: [UInt8], to blob: Blob, offset: Int32) throws -> ResultCode { + try bytes.withUnsafeBytes { buffer -> ResultCode in + let baseAddress = try #require(buffer.baseAddress) + return blob.write(baseAddress, length: Int32(buffer.count), offset: offset) + } +} diff --git a/Tests/LSQLiteTests/Blob/BlobTests.swift b/Tests/LSQLiteTests/Blob/BlobTests.swift new file mode 100644 index 0000000..62c3db6 --- /dev/null +++ b/Tests/LSQLiteTests/Blob/BlobTests.swift @@ -0,0 +1,12 @@ +import LSQLite +import Testing + +@Suite("Blob") +struct BlobRawValueTests { + @Test("init(rawValue:) preserves rawValue") + func rawValueRoundTrip() { + let rawValue = OpaquePointer(bitPattern: 0x3)! + let blob = Blob(rawValue: rawValue) + #expect(blob.rawValue == rawValue) + } +} diff --git a/Tests/LSQLiteTests/Context/Context+AggregateTests.swift b/Tests/LSQLiteTests/Context/Context+AggregateTests.swift new file mode 100644 index 0000000..e141e15 --- /dev/null +++ b/Tests/LSQLiteTests/Context/Context+AggregateTests.swift @@ -0,0 +1,62 @@ +import LSQLite +import Testing + +@Suite("Context+Aggregate") +struct ContextAggregateTests { + @Test("aggregateContext allocates state for aggregates") + func aggregateContextAllocatesState() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + try #require(openDatabase.exec("CREATE TABLE agg(value INTEGER)") == .ok) + try #require(openDatabase.exec("INSERT INTO agg(value) VALUES (1), (2), (3)") == .ok) + + var probe = AggregateProbe() + #expect(openDatabase.createFunction(name: "ctx_count", argumentCount: 1, textEncoding: .utf8, userData: &probe, stepHandler: aggregateStep, finalHandler: aggregateFinal) == .ok) + + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT ctx_count(value) FROM agg", for: openDatabase) == .ok) + let prepared = try #require(statement) + #expect(prepared.step() == .row) + #expect(prepared.columnInt64(at: 0) == 3) + #expect(prepared.step() == .done) + #expect(prepared.finalize() == .ok) + + #expect(probe.allocated) + _ = openDatabase.close() + } +} + +private struct AggregateProbe { + var allocated = false +} + +private func aggregateStep(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context else { + return + } + let wrapper = Context(rawValue: context) + let buffer = wrapper.aggregateContext(size: Int32(MemoryLayout.size)) + if let userData = wrapper.userData { + let probe = userData.assumingMemoryBound(to: AggregateProbe.self) + probe.pointee.allocated = (buffer != nil) + } + if let buffer { + let countPointer = buffer.assumingMemoryBound(to: Int64.self) + countPointer.pointee += 1 + } +} + +private func aggregateFinal(_ context: OpaquePointer?) { + guard let context else { + return + } + let wrapper = Context(rawValue: context) + let buffer = wrapper.aggregateContext(size: 0) + if let buffer { + let countPointer = buffer.assumingMemoryBound(to: Int64.self) + wrapper.resultInt64(countPointer.pointee) + } else { + wrapper.resultInt64(0) + } +} diff --git a/Tests/LSQLiteTests/Context/Context+AuxiliaryTests.swift b/Tests/LSQLiteTests/Context/Context+AuxiliaryTests.swift new file mode 100644 index 0000000..d076810 --- /dev/null +++ b/Tests/LSQLiteTests/Context/Context+AuxiliaryTests.swift @@ -0,0 +1,53 @@ +import LSQLite +import Testing + +@Suite("Context+Auxiliary") +struct ContextAuxiliaryTests { + @Test("setAuxiliaryData and getAuxiliaryData round-trip pointers") + func auxiliaryDataRoundTrip() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + + var probe = ContextAuxiliaryProbe() + #expect(openDatabase.createFunction(name: "ctx_aux", argumentCount: 1, textEncoding: .utf8, userData: &probe, funcHandler: contextAuxiliaryHandler) == .ok) + + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT ctx_aux(1), ctx_aux(1)", for: openDatabase) == .ok) + let prepared = try #require(statement) + #expect(prepared.step() == .row) + #expect(prepared.step() == .done) + #expect(prepared.finalize() == .ok) + + #expect(probe.callCount == 2) + #expect(probe.initialWasNil) + #expect(probe.setCalled) + _ = openDatabase.close() + } +} + +private struct ContextAuxiliaryProbe { + var callCount: Int32 = 0 + var initialWasNil = false + var setCalled = false +} + +private func contextAuxiliaryHandler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context else { + return + } + let wrapper = Context(rawValue: context) + let initial = wrapper.getAuxiliaryData(forArgument: 0) + if let userData = wrapper.userData { + let probe = userData.assumingMemoryBound(to: ContextAuxiliaryProbe.self) + probe.pointee.callCount += 1 + if initial == nil { + probe.pointee.initialWasNil = true + wrapper.setAuxiliaryData(userData, forArgument: 0) + probe.pointee.setCalled = true + } else { + _ = initial == userData + } + } + wrapper.resultInt(1) +} diff --git a/Tests/LSQLiteTests/Context/Context+DatabaseTests.swift b/Tests/LSQLiteTests/Context/Context+DatabaseTests.swift new file mode 100644 index 0000000..fe9326d --- /dev/null +++ b/Tests/LSQLiteTests/Context/Context+DatabaseTests.swift @@ -0,0 +1,41 @@ +import LSQLite +import Testing + +@Suite("Context+Database") +struct ContextDatabaseTests { + @Test("database returns function connection") + func databaseReturnsFunctionConnection() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + + var probe = ContextDatabaseProbe() + #expect(openDatabase.createFunction(name: "ctx_db", argumentCount: 0, textEncoding: .utf8, userData: &probe, funcHandler: contextDatabaseHandler) == .ok) + + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT ctx_db()", for: openDatabase) == .ok) + let prepared = try #require(statement) + #expect(prepared.step() == .row) + #expect(prepared.step() == .done) + #expect(prepared.finalize() == .ok) + + #expect(probe.databasePointer == openDatabase.rawValue) + _ = openDatabase.close() + } +} + +private struct ContextDatabaseProbe { + var databasePointer: OpaquePointer? +} + +private func contextDatabaseHandler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context else { + return + } + let contextWrapper = Context(rawValue: context) + if let userData = contextWrapper.userData { + let probe = userData.assumingMemoryBound(to: ContextDatabaseProbe.self) + probe.pointee.databasePointer = contextWrapper.database?.rawValue + } + contextWrapper.resultInt(1) +} diff --git a/Tests/LSQLiteTests/Context/Context+ResultTests.swift b/Tests/LSQLiteTests/Context/Context+ResultTests.swift new file mode 100644 index 0000000..74fab03 --- /dev/null +++ b/Tests/LSQLiteTests/Context/Context+ResultTests.swift @@ -0,0 +1,264 @@ +import LSQLite +import Testing + +@Suite("Context+Result") +final class ContextResultTests { + private let database: Database + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + self.database = try #require(database) + } + + deinit { + _ = database.close() + } + + @Test("resultInt, resultInt64, and resultDouble set values") + func resultScalarValues() throws { + #expect(database.createFunction(name: "ctx_int", argumentCount: 0, textEncoding: .utf8, funcHandler: resultIntHandler) == .ok) + #expect(database.createFunction(name: "ctx_int64", argumentCount: 0, textEncoding: .utf8, funcHandler: resultInt64Handler) == .ok) + #expect(database.createFunction(name: "ctx_double", argumentCount: 0, textEncoding: .utf8, funcHandler: resultDoubleHandler) == .ok) + + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT ctx_int()", for: database) == .ok) + let intStatement = try #require(statement) + #expect(intStatement.step() == .row) + #expect(intStatement.columnInt(at: 0) == 7) + #expect(intStatement.step() == .done) + #expect(intStatement.finalize() == .ok) + + var int64Statement: Statement? + try #require(Statement.prepare(&int64Statement, sql: "SELECT ctx_int64()", for: database) == .ok) + let preparedInt64 = try #require(int64Statement) + #expect(preparedInt64.step() == .row) + #expect(preparedInt64.columnInt64(at: 0) == 9_000_000_000) + #expect(preparedInt64.step() == .done) + #expect(preparedInt64.finalize() == .ok) + + var doubleStatement: Statement? + try #require(Statement.prepare(&doubleStatement, sql: "SELECT ctx_double()", for: database) == .ok) + let preparedDouble = try #require(doubleStatement) + #expect(preparedDouble.step() == .row) + #expect(preparedDouble.columnDouble(at: 0) == 3.25) + #expect(preparedDouble.step() == .done) + #expect(preparedDouble.finalize() == .ok) + } + + @Test("resultText, resultNull, and resultValue set values") + func resultTextNullAndValue() throws { + #expect(database.createFunction(name: "ctx_text", argumentCount: 0, textEncoding: .utf8, funcHandler: resultTextHandler) == .ok) + #expect(database.createFunction(name: "ctx_null", argumentCount: 0, textEncoding: .utf8, funcHandler: resultNullHandler) == .ok) + #expect(database.createFunction(name: "ctx_value", argumentCount: 1, textEncoding: .utf8, funcHandler: resultValueHandler) == .ok) + + var textStatement: Statement? + try #require(Statement.prepare(&textStatement, sql: "SELECT ctx_text()", for: database) == .ok) + let preparedText = try #require(textStatement) + #expect(preparedText.step() == .row) + #expect(preparedText.columnText(at: 0) == "hello") + #expect(preparedText.step() == .done) + #expect(preparedText.finalize() == .ok) + + var nullStatement: Statement? + try #require(Statement.prepare(&nullStatement, sql: "SELECT ctx_null()", for: database) == .ok) + let preparedNull = try #require(nullStatement) + #expect(preparedNull.step() == .row) + #expect(preparedNull.columnType(at: 0) == .null) + #expect(preparedNull.step() == .done) + #expect(preparedNull.finalize() == .ok) + + var valueStatement: Statement? + try #require(Statement.prepare(&valueStatement, sql: "SELECT ctx_value(42)", for: database) == .ok) + let preparedValue = try #require(valueStatement) + #expect(preparedValue.step() == .row) + #expect(preparedValue.columnInt(at: 0) == 42) + #expect(preparedValue.step() == .done) + #expect(preparedValue.finalize() == .ok) + } + + @Test("resultBlob variants set blob values") + func resultBlobVariants() throws { + #expect(database.createFunction(name: "ctx_blob", argumentCount: 0, textEncoding: .utf8, funcHandler: resultBlobHandler) == .ok) + #expect(database.createFunction(name: "ctx_transient_blob", argumentCount: 0, textEncoding: .utf8, funcHandler: resultTransientBlobHandler) == .ok) + #expect(database.createFunction(name: "ctx_static_blob", argumentCount: 0, textEncoding: .utf8, funcHandler: resultStaticBlobHandler) == .ok) + #expect(database.createFunction(name: "ctx_zero_blob", argumentCount: 0, textEncoding: .utf8, funcHandler: resultZeroBlobHandler) == .ok) + + var blobStatement: Statement? + try #require(Statement.prepare(&blobStatement, sql: "SELECT ctx_blob()", for: database) == .ok) + let preparedBlob = try #require(blobStatement) + #expect(preparedBlob.step() == .row) + #expect(preparedBlob.columnBytes(at: 0) == 2) + #expect(preparedBlob.step() == .done) + #expect(preparedBlob.finalize() == .ok) + + var transientStatement: Statement? + try #require(Statement.prepare(&transientStatement, sql: "SELECT ctx_transient_blob()", for: database) == .ok) + let preparedTransient = try #require(transientStatement) + #expect(preparedTransient.step() == .row) + #expect(preparedTransient.columnBytes(at: 0) == 3) + #expect(preparedTransient.step() == .done) + #expect(preparedTransient.finalize() == .ok) + + var staticStatement: Statement? + try #require(Statement.prepare(&staticStatement, sql: "SELECT ctx_static_blob()", for: database) == .ok) + let preparedStatic = try #require(staticStatement) + #expect(preparedStatic.step() == .row) + #expect(preparedStatic.columnBytes(at: 0) == 2) + #expect(preparedStatic.step() == .done) + #expect(preparedStatic.finalize() == .ok) + + var zeroStatement: Statement? + try #require(Statement.prepare(&zeroStatement, sql: "SELECT ctx_zero_blob()", for: database) == .ok) + let preparedZero = try #require(zeroStatement) + #expect(preparedZero.step() == .row) + #expect(preparedZero.columnBytes(at: 0) == 4) + #expect(preparedZero.step() == .done) + #expect(preparedZero.finalize() == .ok) + } + + @Test("error result helpers produce expected codes") + func errorResultHelpers() throws { + #expect(database.createFunction(name: "ctx_error", argumentCount: 0, textEncoding: .utf8, funcHandler: resultErrorHandler) == .ok) + #expect(database.createFunction(name: "ctx_error_code", argumentCount: 0, textEncoding: .utf8, funcHandler: resultErrorCodeHandler) == .ok) + #expect(database.createFunction(name: "ctx_toobig", argumentCount: 0, textEncoding: .utf8, funcHandler: resultTooBigHandler) == .ok) + #expect(database.createFunction(name: "ctx_nomem", argumentCount: 0, textEncoding: .utf8, funcHandler: resultNoMemoryHandler) == .ok) + + var errorStatement: Statement? + try #require(Statement.prepare(&errorStatement, sql: "SELECT ctx_error()", for: database) == .ok) + let preparedError = try #require(errorStatement) + let errorResult = preparedError.step() + #expect(errorResult != .row && errorResult != .done) + _ = preparedError.finalize() + + var errorCodeStatement: Statement? + try #require(Statement.prepare(&errorCodeStatement, sql: "SELECT ctx_error_code()", for: database) == .ok) + let preparedErrorCode = try #require(errorCodeStatement) + let errorCodeResult = preparedErrorCode.step() + #expect(errorCodeResult != .row && errorCodeResult != .done) + _ = preparedErrorCode.finalize() + + var tooBigStatement: Statement? + try #require(Statement.prepare(&tooBigStatement, sql: "SELECT ctx_toobig()", for: database) == .ok) + let preparedTooBig = try #require(tooBigStatement) + let tooBigResult = preparedTooBig.step() + #expect(tooBigResult != .row && tooBigResult != .done) + _ = preparedTooBig.finalize() + + var noMemoryStatement: Statement? + try #require(Statement.prepare(&noMemoryStatement, sql: "SELECT ctx_nomem()", for: database) == .ok) + let preparedNoMem = try #require(noMemoryStatement) + let noMemoryResult = preparedNoMem.step() + #expect(noMemoryResult != .row && noMemoryResult != .done) + _ = preparedNoMem.finalize() + } + + @Test("resultSubtype assigns subtype when available") + @available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *) + func resultSubtypeAssignsSubtype() throws { + #expect(database.createFunction(name: "ctx_subtype", argumentCount: 0, textEncoding: .utf8, flags: [.resultSubtype], funcHandler: resultSubtypeHandler) == .ok) + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT ctx_subtype()", for: database) == .ok) + let prepared = try #require(statement) + #expect(prepared.step() == .row) + #expect(prepared.step() == .done) + #expect(prepared.finalize() == .ok) + } +} + +private func resultIntHandler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context else { return } + Context(rawValue: context).resultInt(7) +} + +private func resultInt64Handler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context else { return } + Context(rawValue: context).resultInt64(9_000_000_000) +} + +private func resultDoubleHandler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context else { return } + Context(rawValue: context).resultDouble(3.25) +} + +private func resultTextHandler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context else { return } + Context(rawValue: context).resultText("hello") +} + +private func resultNullHandler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context else { return } + Context(rawValue: context).resultNull() +} + +private func resultValueHandler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context, let values else { return } + let wrapper = Context(rawValue: context) + let value = Value(rawValue: values[0]!) + wrapper.resultValue(value) +} + +private func resultBlobHandler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context else { return } + let wrapper = Context(rawValue: context) + let pointer = UnsafeMutableRawPointer.allocate(byteCount: 2, alignment: MemoryLayout.alignment) + pointer.storeBytes(of: UInt8(0x01), as: UInt8.self) + pointer.advanced(by: 1).storeBytes(of: UInt8(0x02), as: UInt8.self) + wrapper.resultBlob(pointer, length: 2, destructor: resultBlobDestructor) +} + +private func resultTransientBlobHandler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context else { return } + let wrapper = Context(rawValue: context) + let bytes: [UInt8] = [0x01, 0x02, 0x03] + bytes.withUnsafeBytes { buffer in + wrapper.resultTransientBlob(buffer.baseAddress!, length: Int32(buffer.count)) + } +} + +private let staticBlobBytes: [UInt8] = [0x0A, 0x0B] + +private func resultStaticBlobHandler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context else { return } + let wrapper = Context(rawValue: context) + staticBlobBytes.withUnsafeBytes { buffer in + wrapper.resultStaticBlob(buffer.baseAddress!, length: Int32(buffer.count)) + } +} + +private func resultZeroBlobHandler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context else { return } + Context(rawValue: context).resultZeroBlob(length: 4) +} + +private func resultErrorHandler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context else { return } + Context(rawValue: context).resultError("failed", length: -1) +} + +private func resultErrorCodeHandler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context else { return } + Context(rawValue: context).resultErrorCode(.constraint) +} + +private func resultTooBigHandler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context else { return } + Context(rawValue: context).resultTooBigError() +} + +private func resultNoMemoryHandler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context else { return } + Context(rawValue: context).resultNoMemoryError() +} + +@available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *) +private func resultSubtypeHandler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context else { return } + let wrapper = Context(rawValue: context) + wrapper.resultInt(1) + wrapper.resultSubtype(Subtype(rawValue: 7)) +} + +private func resultBlobDestructor(_ blob: UnsafeMutableRawPointer?) { + blob?.deallocate() +} diff --git a/Tests/LSQLiteTests/Context/Context+UserDataTests.swift b/Tests/LSQLiteTests/Context/Context+UserDataTests.swift new file mode 100644 index 0000000..f8aabdd --- /dev/null +++ b/Tests/LSQLiteTests/Context/Context+UserDataTests.swift @@ -0,0 +1,41 @@ +import LSQLite +import Testing + +@Suite("Context+UserData") +struct ContextUserDataTests { + @Test("userData returns the registered pointer") + func userDataReturnsRegisteredPointer() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + + var probe = ContextUserDataProbe() + #expect(openDatabase.createFunction(name: "ctx_user_data", argumentCount: 0, textEncoding: .utf8, userData: &probe, funcHandler: contextUserDataHandler) == .ok) + + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT ctx_user_data()", for: openDatabase) == .ok) + let prepared = try #require(statement) + #expect(prepared.step() == .row) + #expect(prepared.step() == .done) + #expect(prepared.finalize() == .ok) + + #expect(probe.matched) + _ = openDatabase.close() + } +} + +private struct ContextUserDataProbe { + var matched = false +} + +private func contextUserDataHandler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context else { + return + } + let wrapper = Context(rawValue: context) + if let userData = wrapper.userData { + let probe = userData.assumingMemoryBound(to: ContextUserDataProbe.self) + probe.pointee.matched = true + } + wrapper.resultInt(1) +} diff --git a/Tests/LSQLiteTests/Context/ContextTests.swift b/Tests/LSQLiteTests/Context/ContextTests.swift new file mode 100644 index 0000000..ea80904 --- /dev/null +++ b/Tests/LSQLiteTests/Context/ContextTests.swift @@ -0,0 +1,12 @@ +import LSQLite +import Testing + +@Suite("Context") +struct ContextRawValueTests { + @Test("init(rawValue:) preserves rawValue") + func rawValueRoundTrip() { + let rawValue = OpaquePointer(bitPattern: 0x5)! + let context = Context(rawValue: rawValue) + #expect(context.rawValue == rawValue) + } +} diff --git a/Tests/LSQLiteTests/Database/Database+AuthorizerTests.swift b/Tests/LSQLiteTests/Database/Database+AuthorizerTests.swift new file mode 100644 index 0000000..263c211 --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+AuthorizerTests.swift @@ -0,0 +1,118 @@ +import LSQLite +import MissedSwiftSQLite +import Testing + +@Suite("Database+Authorizer") +struct DatabaseAuthorizerTests { + @Test("AuthorizerHandlerResult init(rawValue:) preserves rawValue") + func authorizerHandlerResultRawValueRoundTrip() { + let rawValue = Int32(8) + let result = Database.AuthorizerHandlerResult(rawValue: rawValue) + #expect(result.rawValue == rawValue) + } + + @Test("AuthorizerHandlerActionCode init(rawValue:) preserves rawValue") + func authorizerHandlerActionCodeRawValueRoundTrip() { + let rawValue = Int32(9) + let actionCode = Database.AuthorizerHandlerActionCode(rawValue: rawValue) + #expect(actionCode.rawValue == rawValue) + } + + @Test("AuthorizerHandlerResult constants match SQLite") + func authorizerHandlerResultConstantsMatchSQLite() { + #expect(Database.AuthorizerHandlerResult.ok.rawValue == SQLITE_OK) + #expect(Database.AuthorizerHandlerResult.deny.rawValue == SQLITE_DENY) + #expect(Database.AuthorizerHandlerResult.ignore.rawValue == SQLITE_IGNORE) + } + + @Test("AuthorizerHandlerActionCode constants match SQLite") + func authorizerHandlerActionCodeConstantsMatchSQLite() { + #expect(Database.AuthorizerHandlerActionCode.createIndex.rawValue == SQLITE_CREATE_INDEX) + #expect(Database.AuthorizerHandlerActionCode.createTable.rawValue == SQLITE_CREATE_TABLE) + #expect(Database.AuthorizerHandlerActionCode.createTempIndex.rawValue == SQLITE_CREATE_TEMP_INDEX) + #expect(Database.AuthorizerHandlerActionCode.createTempTable.rawValue == SQLITE_CREATE_TEMP_TABLE) + #expect(Database.AuthorizerHandlerActionCode.createTempTrigger.rawValue == SQLITE_CREATE_TEMP_TRIGGER) + #expect(Database.AuthorizerHandlerActionCode.createTempView.rawValue == SQLITE_CREATE_TEMP_VIEW) + #expect(Database.AuthorizerHandlerActionCode.createTrigger.rawValue == SQLITE_CREATE_TRIGGER) + #expect(Database.AuthorizerHandlerActionCode.createView.rawValue == SQLITE_CREATE_VIEW) + #expect(Database.AuthorizerHandlerActionCode.delete.rawValue == SQLITE_DELETE) + #expect(Database.AuthorizerHandlerActionCode.dropIndex.rawValue == SQLITE_DROP_INDEX) + #expect(Database.AuthorizerHandlerActionCode.dropTable.rawValue == SQLITE_DROP_TABLE) + #expect(Database.AuthorizerHandlerActionCode.dropTempIndex.rawValue == SQLITE_DROP_TEMP_INDEX) + #expect(Database.AuthorizerHandlerActionCode.dropTempTable.rawValue == SQLITE_DROP_TEMP_TABLE) + #expect(Database.AuthorizerHandlerActionCode.dropTempTrigger.rawValue == SQLITE_DROP_TEMP_TRIGGER) + #expect(Database.AuthorizerHandlerActionCode.dropTempView.rawValue == SQLITE_DROP_TEMP_VIEW) + #expect(Database.AuthorizerHandlerActionCode.dropTrigger.rawValue == SQLITE_DROP_TRIGGER) + #expect(Database.AuthorizerHandlerActionCode.dropView.rawValue == SQLITE_DROP_VIEW) + #expect(Database.AuthorizerHandlerActionCode.insert.rawValue == SQLITE_INSERT) + #expect(Database.AuthorizerHandlerActionCode.pragma.rawValue == SQLITE_PRAGMA) + #expect(Database.AuthorizerHandlerActionCode.read.rawValue == SQLITE_READ) + #expect(Database.AuthorizerHandlerActionCode.select.rawValue == SQLITE_SELECT) + #expect(Database.AuthorizerHandlerActionCode.transaction.rawValue == SQLITE_TRANSACTION) + #expect(Database.AuthorizerHandlerActionCode.update.rawValue == SQLITE_UPDATE) + #expect(Database.AuthorizerHandlerActionCode.attach.rawValue == SQLITE_ATTACH) + #expect(Database.AuthorizerHandlerActionCode.detach.rawValue == SQLITE_DETACH) + #expect(Database.AuthorizerHandlerActionCode.alterTable.rawValue == SQLITE_ALTER_TABLE) + #expect(Database.AuthorizerHandlerActionCode.reindex.rawValue == SQLITE_REINDEX) + #expect(Database.AuthorizerHandlerActionCode.analyze.rawValue == SQLITE_ANALYZE) + #expect(Database.AuthorizerHandlerActionCode.createVTable.rawValue == SQLITE_CREATE_VTABLE) + #expect(Database.AuthorizerHandlerActionCode.dropVTable.rawValue == SQLITE_DROP_VTABLE) + #expect(Database.AuthorizerHandlerActionCode.function.rawValue == SQLITE_FUNCTION) + #expect(Database.AuthorizerHandlerActionCode.savepoint.rawValue == SQLITE_SAVEPOINT) + #expect(Database.AuthorizerHandlerActionCode.copy.rawValue == SQLITE_COPY) + #expect(Database.AuthorizerHandlerActionCode.recursive.rawValue == SQLITE_RECURSIVE) + } + + @Test("AuthorizerHandlerResult descriptions map values") + func authorizerHandlerResultDescriptions() { + #expect(Database.AuthorizerHandlerResult.ok.description == "ok") + #expect(Database.AuthorizerHandlerResult.deny.description == "deny") + #expect(Database.AuthorizerHandlerResult.ignore.description == "ignore") + #expect(Database.AuthorizerHandlerResult(rawValue: 77).description == "unknown") + #expect(Database.AuthorizerHandlerResult.ok.debugDescription == "SQLITE_OK") + #expect(Database.AuthorizerHandlerResult(rawValue: 77).debugDescription == "77") + } + + @Test("AuthorizerHandlerActionCode descriptions map values") + func authorizerHandlerActionCodeDescriptions() { + #expect(Database.AuthorizerHandlerActionCode.createIndex.description == "create index") + #expect(Database.AuthorizerHandlerActionCode.dropTable.description == "drop table") + #expect(Database.AuthorizerHandlerActionCode.select.description == "select") + #expect(Database.AuthorizerHandlerActionCode(rawValue: -1).description == "unknown") + #expect(Database.AuthorizerHandlerActionCode.createIndex.debugDescription == "SQLITE_CREATE_INDEX") + #expect(Database.AuthorizerHandlerActionCode(rawValue: -1).debugDescription == "-1") + } + + @Test("setAuthorizerHandler registers callback") + func setAuthorizerHandlerRegistersCallback() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + try #require(openDatabase.exec("CREATE TABLE auth(id INTEGER)") == .ok) + + var probe = AuthorizerProbe() + #expect(openDatabase.setAuthorizerHandler(userData: &probe, authorizerHandler) == .ok) + + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT id FROM auth", for: openDatabase) == .ok) + let prepared = try #require(statement) + #expect(prepared.finalize() == .ok) + + #expect(probe.called) + #expect(openDatabase.setAuthorizerHandler(userData: nil, nil) == .ok) + _ = openDatabase.close() + } +} + +private struct AuthorizerProbe { + var called = false +} + +private func authorizerHandler(_ userData: UnsafeMutableRawPointer?, _ actionCode: Int32, _ detail1: UnsafePointer?, _ detail2: UnsafePointer?, _ databaseName: UnsafePointer?, _ triggerOrViewName: UnsafePointer?) -> Int32 { + guard let userData else { + return Database.AuthorizerHandlerResult.ok.rawValue + } + let probe = userData.assumingMemoryBound(to: AuthorizerProbe.self) + probe.pointee.called = true + return Database.AuthorizerHandlerResult.ok.rawValue +} diff --git a/Tests/LSQLiteTests/Database/Database+AutocommitTests.swift b/Tests/LSQLiteTests/Database/Database+AutocommitTests.swift new file mode 100644 index 0000000..225f808 --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+AutocommitTests.swift @@ -0,0 +1,28 @@ +import LSQLite +import Testing + +@Suite("Database+Autocommit") +final class DatabaseAutocommitTests { + private let database: Database + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + self.database = try #require(database) + } + + deinit { + _ = database.close() + } + + @Test("isAutocommit reflects transaction state") + func autocommitReflectsTransactionState() throws { + #expect(database.isAutocommit) + + try #require(database.exec("BEGIN") == .ok) + #expect(!database.isAutocommit) + + try #require(database.exec("COMMIT") == .ok) + #expect(database.isAutocommit) + } +} diff --git a/Tests/LSQLiteTests/Database/Database+BlobTests.swift b/Tests/LSQLiteTests/Database/Database+BlobTests.swift new file mode 100644 index 0000000..88bb811 --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+BlobTests.swift @@ -0,0 +1,36 @@ +import LSQLite +import Testing + +@Suite("Database+Blob") +struct DatabaseOpenBlobFlagRawValueTests { + @Test("init(rawValue:) preserves rawValue") + func rawValueRoundTrip() { + let rawValue = Int32(2) + let openBlobFlag = Database.OpenBlobFlag(rawValue: rawValue) + #expect(openBlobFlag.rawValue == rawValue) + } + + @Test("OpenBlobFlag descriptions map values") + func openBlobFlagDescriptions() { + #expect(Database.OpenBlobFlag.readonly.description == "readonly") + #expect(Database.OpenBlobFlag.readwrite.description == "readwrite") + #expect(Database.OpenBlobFlag(rawValue: 5).description == "5") + #expect(Database.OpenBlobFlag.readonly.debugDescription == "readonly (0)") + } + + @Test("openBlob returns a blob handle") + func openBlobReturnsBlobHandle() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + try #require(openDatabase.exec("CREATE TABLE blobs(data BLOB NOT NULL)") == .ok) + try #require(openDatabase.exec("INSERT INTO blobs(data) VALUES (zeroblob(2))") == .ok) + let rowID = openDatabase.lastInsertedRowID() + + var blob: Blob? + #expect(openDatabase.openBlob(&blob, databaseName: "main", tableName: "blobs", columnName: "data", rowID: rowID, flags: .readonly) == .ok) + let openedBlob = try #require(blob) + #expect(openedBlob.close() == .ok) + _ = openDatabase.close() + } +} diff --git a/Tests/LSQLiteTests/Database/Database+BusyTests.swift b/Tests/LSQLiteTests/Database/Database+BusyTests.swift new file mode 100644 index 0000000..c8b0053 --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+BusyTests.swift @@ -0,0 +1,44 @@ +import LSQLite +import Testing + +@Suite("Database+Busy") +final class DatabaseBusyHandlerResultRawValueTests { + private let database: Database + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + self.database = try #require(database) + } + + deinit { + _ = database.close() + } + + @Test("init(rawValue:) preserves rawValue") + func rawValueRoundTrip() { + let rawValue = Int32(17) + let busyHandlerResult = Database.BusyHandlerResult(rawValue: rawValue) + #expect(busyHandlerResult.rawValue == rawValue) + } + + @Test("BusyHandlerResult descriptions map values") + func busyHandlerResultDescriptions() { + #expect(Database.BusyHandlerResult.break.description == "break") + #expect(Database.BusyHandlerResult.continue.description == "continue") + #expect(Database.BusyHandlerResult(rawValue: 99).description == "unknown") + #expect(Database.BusyHandlerResult.break.debugDescription == "break (0)") + #expect(Database.BusyHandlerResult(rawValue: 99).debugDescription == "unknown (99)") + } + + @Test("setBusyHandler registers and clears handler") + func setBusyHandlerRegistersAndClearsHandler() { + #expect(database.setBusyHandler(busyHandler) == .ok) + database.setTimerBusyHandler(milliseconds: 1) + #expect(database.setBusyHandler(userData: nil, nil) == .ok) + } +} + +private func busyHandler(_ userData: UnsafeMutableRawPointer?, _ attempt: Int32) -> Int32 { + Database.BusyHandlerResult.break.rawValue +} diff --git a/Tests/LSQLiteTests/Database/Database+ChangesTests.swift b/Tests/LSQLiteTests/Database/Database+ChangesTests.swift new file mode 100644 index 0000000..fdb0194 --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+ChangesTests.swift @@ -0,0 +1,30 @@ +import LSQLite +import Testing + +@Suite("Database+Changes") +final class DatabaseChangesTests { + private let database: Database + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + self.database = openDatabase + try #require(openDatabase.exec("CREATE TABLE changes(value TEXT)") == .ok) + } + + deinit { + _ = database.close() + } + + @Test("changes and totalChanges track writes") + func changesAndTotalChangesTrackWrites() throws { + try #require(database.exec("INSERT INTO changes(value) VALUES ('a')") == .ok) + #expect(database.changes == 1) + let totalAfterInsert = database.totalChanges + + try #require(database.exec("UPDATE changes SET value = 'b'") == .ok) + #expect(database.changes == 1) + #expect(database.totalChanges == totalAfterInsert + 1) + } +} diff --git a/Tests/LSQLiteTests/Database/Database+CheckpointTests.swift b/Tests/LSQLiteTests/Database/Database+CheckpointTests.swift new file mode 100644 index 0000000..721ff6d --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+CheckpointTests.swift @@ -0,0 +1,55 @@ +import LSQLite +import MissedSwiftSQLite +import Testing + +@Suite("Database+Checkpoint") +final class DatabaseCheckpointModeRawValueTests { + private let database: Database + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + self.database = try #require(database) + } + + deinit { + _ = database.close() + } + + @Test("init(rawValue:) preserves rawValue") + func rawValueRoundTrip() { + let rawValue = Int32(9) + let mode = Database.CheckpointMode(rawValue: rawValue) + #expect(mode.rawValue == rawValue) + } + + @Test("CheckpointMode constants match SQLite") + func checkpointModeConstantsMatchSQLite() { + #expect(Database.CheckpointMode.passive.rawValue == SQLITE_CHECKPOINT_PASSIVE) + #expect(Database.CheckpointMode.full.rawValue == SQLITE_CHECKPOINT_FULL) + #expect(Database.CheckpointMode.restart.rawValue == SQLITE_CHECKPOINT_RESTART) + #expect(Database.CheckpointMode.truncate.rawValue == SQLITE_CHECKPOINT_TRUNCATE) + } + + @Test("CheckpointMode descriptions map values") + func checkpointModeDescriptions() { + #expect(Database.CheckpointMode.passive.description == "passive") + #expect(Database.CheckpointMode.full.description == "full") + #expect(Database.CheckpointMode.restart.description == "restart") + #expect(Database.CheckpointMode.truncate.description == "truncate") + #expect(Database.CheckpointMode(rawValue: -2).description == "unknown") + #expect(Database.CheckpointMode.passive.debugDescription == "SQLITE_CHECKPOINT_PASSIVE") + #expect(Database.CheckpointMode(rawValue: -2).debugDescription == "-2") + } + + @Test("autoWALCheckpoint and walCheckpoint return results") + func autoWALCheckpointAndWalCheckpointReturnResults() { + #expect(database.autoWALCheckpoint(pageInWALFileCount: 0) == .ok) + var frameCount: Int32 = -1 + var totalFrameCount: Int32 = -1 + let result = database.walCheckpoint("main", mode: .passive, frameCount: &frameCount, totalFrameCount: &totalFrameCount) + #expect(result == .ok) + #expect(frameCount >= -1) + #expect(totalFrameCount >= -1) + } +} diff --git a/Tests/LSQLiteTests/Database/Database+CloseTests.swift b/Tests/LSQLiteTests/Database/Database+CloseTests.swift new file mode 100644 index 0000000..9ef7868 --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+CloseTests.swift @@ -0,0 +1,22 @@ +import LSQLite +import Testing + +@Suite("Database+Close") +struct DatabaseCloseTests { + @Test("close returns ok for open database") + func closeReturnsOk() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + #expect(openDatabase.close() == .ok) + } + + @Test("closeV2 returns ok for open database") + @available(iOS 8.2, macOS 10.10, tvOS 8.2, watchOS 2.0, *) + func closeV2ReturnsOk() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + #expect(openDatabase.closeV2() == .ok) + } +} diff --git a/Tests/LSQLiteTests/Database/Database+CollationTests.swift b/Tests/LSQLiteTests/Database/Database+CollationTests.swift new file mode 100644 index 0000000..acbc3ea --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+CollationTests.swift @@ -0,0 +1,81 @@ +import LSQLite +import MissedSwiftSQLite +import Testing + +@Suite("Database+Collation") +final class DatabaseCollationFlagRawValueTests { + private let database: Database + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + self.database = try #require(database) + } + + deinit { + _ = database.close() + } + + @Test("init(rawValue:) preserves rawValue") + func rawValueRoundTrip() { + let rawValue = Int32(3) + let collationFlag = Database.CollationFlag(rawValue: rawValue) + #expect(collationFlag.rawValue == rawValue) + } + + @Test("CollationFlag constants match SQLite") + func collationFlagConstantsMatchSQLite() { + #expect(Database.CollationFlag.utf8.rawValue == SQLITE_UTF8) + #expect(Database.CollationFlag.utf16le.rawValue == SQLITE_UTF16LE) + #expect(Database.CollationFlag.utf16be.rawValue == SQLITE_UTF16BE) + #expect(Database.CollationFlag.utf16.rawValue == SQLITE_UTF16) + #expect(Database.CollationFlag.utf16Aligned.rawValue == SQLITE_UTF16_ALIGNED) + } + + @Test("CollationFlag descriptions map values") + func collationFlagDescriptions() { + #expect(Database.CollationFlag.utf8.description == "utf8") + #expect(Database.CollationFlag.utf16le.description == "utf16le") + #expect(Database.CollationFlag.utf16be.description == "utf16be") + #expect(Database.CollationFlag.utf16.description == "utf16") + #expect(Database.CollationFlag.utf16Aligned.description == "utf16Aligned") + #expect(Database.CollationFlag(rawValue: -2).description == "unknown") + #expect(Database.CollationFlag.utf8.debugDescription == "SQLITE_UTF8") + #expect(Database.CollationFlag(rawValue: -2).debugDescription == "-2") + } + + @Test("createCollation registers a comparator") + func createCollationRegistersComparator() { + #expect(database.createCollation(name: "always_equal", flag: .utf8, compareHandler: alwaysEqualCollation) == .ok) + } + + @Test("collationNeeded registers and invokes handler") + func collationNeededRegistersAndInvokesHandler() throws { + var probe = CollationNeededProbe() + #expect(database.collationNeeded(userData: &probe, neededHandler: collationNeededHandler) == .ok) + try #require(database.exec("SELECT 'a' = 'b' COLLATE needs_help") == .ok) + #expect(probe.called) + #expect(database.collationNeeded(userData: nil, neededHandler: nil) == .ok) + } +} + +private struct CollationNeededProbe { + var called = false +} + +private func alwaysEqualCollation(_ userData: UnsafeMutableRawPointer?, _ lhsLength: Int32, _ lhs: UnsafeRawPointer?, _ rhsLength: Int32, _ rhs: UnsafeRawPointer?) -> Int32 { + 0 +} + +private func collationNeededHandler(_ userData: UnsafeMutableRawPointer?, _ database: OpaquePointer?, _ collationFlag: Int32, _ name: UnsafePointer?) { + guard let userData else { + return + } + let probe = userData.assumingMemoryBound(to: CollationNeededProbe.self) + probe.pointee.called = true + guard let database, let name else { + return + } + let connection = Database(rawValue: database) + _ = connection.createCollation(name: String(cString: name), flag: Database.CollationFlag(rawValue: collationFlag), compareHandler: alwaysEqualCollation) +} diff --git a/Tests/LSQLiteTests/Database/Database+ErrorTests.swift b/Tests/LSQLiteTests/Database/Database+ErrorTests.swift new file mode 100644 index 0000000..bf0754e --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+ErrorTests.swift @@ -0,0 +1,49 @@ +import LSQLite +import Testing + +@Suite("Database+Error") +final class DatabaseExtendedResultCodeStatusRawValueTests { + private let database: Database + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + self.database = openDatabase + try #require(openDatabase.exec("CREATE TABLE items(id INTEGER)") == .ok) + } + + deinit { + _ = database.close() + } + + @Test("init(rawValue:) preserves rawValue") + func rawValueRoundTrip() { + let rawValue = Int32(42) + let status = Database.ExtendedResultCodeStatus(rawValue: rawValue) + #expect(status.rawValue == rawValue) + } + + @Test("ExtendedResultCodeStatus descriptions map values") + func extendedResultCodeStatusDescriptions() { + #expect(Database.ExtendedResultCodeStatus.off.description == "off") + #expect(Database.ExtendedResultCodeStatus.on.description == "on") + #expect(Database.ExtendedResultCodeStatus(rawValue: 3).description == "unknown") + #expect(Database.ExtendedResultCodeStatus.on.debugDescription == "on (1)") + #expect(Database.ExtendedResultCodeStatus(rawValue: 3).debugDescription == "unknown (3)") + } + + @Test("last error fields update after a failure") + func lastErrorFieldsUpdateAfterFailure() throws { + #expect(database.exec("INSERT INTO missing_table VALUES (1)") == .error) + #expect(database.lastErrorCode == .error) + #expect(database.lastExtendedErrorCode == .error) + #expect(!database.lastErrorMessage.isEmpty) + } + + @Test("setExtendedResultCodes toggles status") + func setExtendedResultCodesTogglesStatus() { + #expect(database.setExtendedResultCodes(.on) == .ok) + #expect(database.setExtendedResultCodes(.off) == .ok) + } +} diff --git a/Tests/LSQLiteTests/Database/Database+ExecTests.swift b/Tests/LSQLiteTests/Database/Database+ExecTests.swift new file mode 100644 index 0000000..e448f46 --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+ExecTests.swift @@ -0,0 +1,66 @@ +import LSQLite +import MissedSwiftSQLite +import Testing + +@Suite("Database+Exec") +final class DatabaseExecCallbackResultRawValueTests { + private let database: Database + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + self.database = openDatabase + try #require(openDatabase.exec("CREATE TABLE items(id INTEGER)") == .ok) + try #require(openDatabase.exec("INSERT INTO items(id) VALUES (1), (2)") == .ok) + } + + deinit { + _ = database.close() + } + + @Test("init(rawValue:) preserves rawValue") + func rawValueRoundTrip() { + let rawValue = Int32(4) + let callbackResult = Database.ExecCallbackResult(rawValue: rawValue) + #expect(callbackResult.rawValue == rawValue) + } + + @Test("ExecCallbackResult descriptions map values") + func execCallbackResultDescriptions() { + #expect(Database.ExecCallbackResult.continue.description == "continue") + #expect(Database.ExecCallbackResult.abort.description == "abort") + #expect(Database.ExecCallbackResult(rawValue: 9).description == "unknown") + #expect(Database.ExecCallbackResult.continue.debugDescription == "continue (0)") + #expect(Database.ExecCallbackResult(rawValue: 9).debugDescription == "unknown (9)") + } + + @Test("exec invokes callback for rows") + func execInvokesCallbackForRows() throws { + var rowCount = Int32(0) + let result = database.exec("SELECT id FROM items ORDER BY id", userData: &rowCount, callback: execRowCounter) + #expect(result == .ok) + #expect(rowCount == 2) + } + + @Test("exec captures error messages") + func execCapturesErrorMessages() { + var errorMessage: UnsafeMutablePointer? = nil + defer { + if let errorMessage { + sqlite3_free(errorMessage) + } + } + #expect(database.exec("SELECT * FROM missing_table", errorMessage: &errorMessage) == .error) + #expect(errorMessage != nil) + } +} + +private func execRowCounter(_ userData: UnsafeMutableRawPointer?, _ count: Int32, _ values: UnsafeMutablePointer?>?, _ columns: UnsafeMutablePointer?>?) -> Int32 { + guard let userData else { + return Database.ExecCallbackResult.abort.rawValue + } + let rowCount = userData.assumingMemoryBound(to: Int32.self) + rowCount.pointee += 1 + return Database.ExecCallbackResult.continue.rawValue +} diff --git a/Tests/LSQLiteTests/Database/Database+FilenameTests.swift b/Tests/LSQLiteTests/Database/Database+FilenameTests.swift new file mode 100644 index 0000000..be9988f --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+FilenameTests.swift @@ -0,0 +1,31 @@ +import Foundation +import LSQLite +import Testing + +@Suite("Database+Filename") +struct DatabaseFilenameTests { + @Test("filename returns nil for in-memory database") + func filenameReturnsNilForInMemoryDatabase() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + #expect(openDatabase.filename(forDatabaseNamed: "main") == nil) + #expect(openDatabase.filename(forDatabaseNamed: "missing") == nil) + _ = openDatabase.close() + } + + @Test("filename returns a path for file database") + func filenameReturnsPathForFileDatabase() throws { + let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent("lsqlite-filename.sqlite") + defer { + try? FileManager.default.removeItem(at: fileURL) + } + var database: Database? + try #require(Database.open(&database, at: .init(rawValue: fileURL.path), withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + let filename = openDatabase.filename(forDatabaseNamed: "main") + #expect(filename != nil) + #expect(filename?.isEmpty == false) + _ = openDatabase.close() + } +} diff --git a/Tests/LSQLiteTests/Database/Database+FunctionTests.swift b/Tests/LSQLiteTests/Database/Database+FunctionTests.swift new file mode 100644 index 0000000..d781c69 --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+FunctionTests.swift @@ -0,0 +1,121 @@ +import LSQLite +import MissedSwiftSQLite +import Testing + +@Suite("Database+Function") +final class DatabaseFunctionTests { + private let database: Database + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + self.database = try #require(database) + } + + deinit { + _ = database.close() + } + + @Test("TextEncoding init(rawValue:) preserves rawValue") + func textEncodingRawValueRoundTrip() { + let rawValue = Int32(10) + let encoding = Database.TextEncoding(rawValue: rawValue) + #expect(encoding.rawValue == rawValue) + } + + @Test("FunctionFlag init(rawValue:) preserves rawValue") + func functionFlagRawValueRoundTrip() { + let rawValue = Int32(11) + let flag = Database.FunctionFlag(rawValue: rawValue) + #expect(flag.rawValue == rawValue) + } + + @Test("FunctionFlag init(rawValue:) preserves combined rawValue") + func functionFlagCombinedRawValueRoundTrip() { + let rawValue = Database.FunctionFlag.deterministic.rawValue | Database.FunctionFlag.directOnly.rawValue + let flag = Database.FunctionFlag(rawValue: rawValue) + #expect(flag.rawValue == rawValue) + } + + @Test("TextEncoding constants match SQLite") + func textEncodingConstantsMatchSQLite() { + #expect(Database.TextEncoding.utf8.rawValue == SQLITE_UTF8) + #expect(Database.TextEncoding.utf16le.rawValue == SQLITE_UTF16LE) + #expect(Database.TextEncoding.utf16be.rawValue == SQLITE_UTF16BE) + #expect(Database.TextEncoding.utf16.rawValue == SQLITE_UTF16) + #expect(Database.TextEncoding.any.rawValue == SQLITE_ANY) + } + + @Test("FunctionFlag constants match SQLite") + func functionFlagConstantsMatchSQLite() { + #expect(Database.FunctionFlag.deterministic.rawValue == SQLITE_DETERMINISTIC) + #expect(Database.FunctionFlag.directOnly.rawValue == SQLITE_DIRECTONLY) + #expect(Database.FunctionFlag.subtype.rawValue == SQLITE_SUBTYPE) + #expect(Database.FunctionFlag.innocuous.rawValue == SQLITE_INNOCUOUS) + #expect(Database.FunctionFlag.resultSubtype.rawValue == SQLITE_RESULT_SUBTYPE) + } + + @Test("TextEncoding descriptions map values") + func textEncodingDescriptions() { + #expect(Database.TextEncoding.utf8.description == "utf8") + #expect(Database.TextEncoding.utf16le.description == "utf16le") + #expect(Database.TextEncoding.utf16be.description == "utf16be") + #expect(Database.TextEncoding.utf16.description == "utf16") + #expect(Database.TextEncoding.any.description == "any") + #expect(Database.TextEncoding(rawValue: -1).description == "unknown") + #expect(Database.TextEncoding.utf8.debugDescription == "SQLITE_UTF8") + } + + @Test("FunctionFlag descriptions map values") + func functionFlagDescriptions() { + #expect(Database.FunctionFlag([]).description == "[]") + #expect(Database.FunctionFlag.deterministic.description.contains(".deterministic")) + let knownMask = UInt32(bitPattern: Database.FunctionFlag.deterministic.rawValue) + | UInt32(bitPattern: Database.FunctionFlag.directOnly.rawValue) + | UInt32(bitPattern: Database.FunctionFlag.subtype.rawValue) + | UInt32(bitPattern: Database.FunctionFlag.innocuous.rawValue) + | UInt32(bitPattern: Database.FunctionFlag.resultSubtype.rawValue) + let unknownOnlyRaw = Int32(bitPattern: ~knownMask) + #expect(Database.FunctionFlag(rawValue: unknownOnlyRaw).description == "unknown") + let mixed = Database.FunctionFlag(rawValue: Database.FunctionFlag.deterministic.rawValue | unknownOnlyRaw) + #expect(mixed.description.contains("unknown")) + #expect(Database.FunctionFlag.deterministic.debugDescription.contains("SQLITE_DETERMINISTIC")) + #expect(mixed.debugDescription.contains("0x")) + } + + @Test("createFunction registers scalar function") + func createFunctionRegistersScalarFunction() throws { + #expect(database.createFunction(name: "constant_seven", argumentCount: 0, textEncoding: .utf8, flags: [.deterministic], funcHandler: constantFunction) == .ok) + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT constant_seven()", for: database) == .ok) + let prepared = try #require(statement) + #expect(prepared.step() == .row) + #expect(prepared.columnInt(at: 0) == 7) + #expect(prepared.step() == .done) + #expect(prepared.finalize() == .ok) + } + + @Test("createWindowFunction registers window function") + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + func createWindowFunctionRegistersWindowFunction() { + #expect(database.createWindowFunction(name: "window_zero", argumentCount: 0, textEncoding: .utf8, flags: [], stepHandler: windowStep, finalHandler: windowFinal, valueHandler: nil, inverseHandler: nil) == .ok) + } +} + +private func constantFunction(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context else { + return + } + let contextWrapper = Context(rawValue: context) + contextWrapper.resultInt(7) +} + +private func windowStep(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { +} + +private func windowFinal(_ context: OpaquePointer?) { + guard let context else { + return + } + Context(rawValue: context).resultInt(0) +} diff --git a/Tests/LSQLiteTests/Database/Database+HooksTests.swift b/Tests/LSQLiteTests/Database/Database+HooksTests.swift new file mode 100644 index 0000000..effb24b --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+HooksTests.swift @@ -0,0 +1,124 @@ +import LSQLite +import MissedSwiftSQLite +import Testing + +@Suite("Database+Hooks") +final class DatabaseHooksTests { + private let database: Database + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + self.database = openDatabase + try #require(openDatabase.exec("CREATE TABLE hooks(id INTEGER)") == .ok) + } + + deinit { + _ = database.close() + } + + @Test("CommitHookHandlerResult init(rawValue:) preserves rawValue") + func commitHookHandlerResultRawValueRoundTrip() { + let rawValue = Int32(6) + let result = Database.CommitHookHandlerResult(rawValue: rawValue) + #expect(result.rawValue == rawValue) + } + + @Test("UpdateOperation init(rawValue:) preserves rawValue") + func updateOperationRawValueRoundTrip() { + let rawValue = Int32(7) + let operation = Database.UpdateOperation(rawValue: rawValue) + #expect(operation.rawValue == rawValue) + } + + @Test("UpdateOperation constants match SQLite") + func updateOperationConstantsMatchSQLite() { + #expect(Database.UpdateOperation.delete.rawValue == SQLITE_DELETE) + #expect(Database.UpdateOperation.insert.rawValue == SQLITE_INSERT) + #expect(Database.UpdateOperation.update.rawValue == SQLITE_UPDATE) + } + + @Test("hook result and operation descriptions map values") + func hookDescriptionsMapValues() { + let commitContinue = Database.CommitHookHandlerResult.continue + #expect(!commitContinue.description.isEmpty) + #expect(commitContinue.debugDescription.contains(commitContinue.description)) + + let commitUnknown = Database.CommitHookHandlerResult(rawValue: 9) + #expect(!commitUnknown.description.isEmpty) + #expect(commitUnknown.debugDescription.contains("9")) + + let updateDelete = Database.UpdateOperation.delete + #expect(!updateDelete.description.isEmpty) + #expect(updateDelete.debugDescription.contains("SQLITE_DELETE")) + + let updateUnknown = Database.UpdateOperation(rawValue: 999) + #expect(!updateUnknown.description.isEmpty) + #expect(updateUnknown.debugDescription.contains("999")) + } + + @Test("commit, rollback, update, and WAL hooks register and run") + func hooksRegisterAndRun() throws { + var probe = HookProbe() + _ = database.commitHook(&probe, commitHookHandler: commitHook) + _ = database.rollbackHook(&probe, rollbackHookHandler: rollbackHook) + _ = database.updateHook(&probe, updateHookHandler: updateHook) + _ = database.walHook(&probe, walHookHandler: walHook) + + try #require(database.exec("BEGIN") == .ok) + try #require(database.exec("INSERT INTO hooks(id) VALUES (1)") == .ok) + try #require(database.exec("COMMIT") == .ok) + + #expect(probe.commitCalls == 1) + #expect(probe.updateCalls == 1) + #expect(probe.lastUpdateOperation == Database.UpdateOperation.insert.rawValue) + + try #require(database.exec("BEGIN") == .ok) + try #require(database.exec("ROLLBACK") == .ok) + #expect(probe.rollbackCalls == 1) + } +} + +private struct HookProbe { + var commitCalls: Int32 = 0 + var rollbackCalls: Int32 = 0 + var updateCalls: Int32 = 0 + var walCalls: Int32 = 0 + var lastUpdateOperation: Int32 = 0 +} + +private func commitHook(_ userData: UnsafeMutableRawPointer?) -> Int32 { + guard let userData else { + return Database.CommitHookHandlerResult.break.rawValue + } + let probe = userData.assumingMemoryBound(to: HookProbe.self) + probe.pointee.commitCalls += 1 + return Database.CommitHookHandlerResult.continue.rawValue +} + +private func rollbackHook(_ userData: UnsafeMutableRawPointer?) { + guard let userData else { + return + } + let probe = userData.assumingMemoryBound(to: HookProbe.self) + probe.pointee.rollbackCalls += 1 +} + +private func updateHook(_ userData: UnsafeMutableRawPointer?, _ updateOperation: Int32, _ databaseName: UnsafePointer?, _ tableName: UnsafePointer?, _ rowID: Int64) { + guard let userData else { + return + } + let probe = userData.assumingMemoryBound(to: HookProbe.self) + probe.pointee.updateCalls += 1 + probe.pointee.lastUpdateOperation = updateOperation +} + +private func walHook(_ userData: UnsafeMutableRawPointer?, _ database: OpaquePointer?, _ databaseName: UnsafePointer?, _ pageInWALFileCount: Int32) -> Int32 { + guard let userData else { + return 0 + } + let probe = userData.assumingMemoryBound(to: HookProbe.self) + probe.pointee.walCalls += 1 + return 0 +} diff --git a/Tests/LSQLiteTests/Database/Database+InterruptTests.swift b/Tests/LSQLiteTests/Database/Database+InterruptTests.swift new file mode 100644 index 0000000..35cf87e --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+InterruptTests.swift @@ -0,0 +1,14 @@ +import LSQLite +import Testing + +@Suite("Database+Interrupt") +struct DatabaseInterruptTests { + @Test("interrupt can be called on open database") + func interruptCanBeCalledOnOpenDatabase() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + openDatabase.interrupt() + _ = openDatabase.close() + } +} diff --git a/Tests/LSQLiteTests/Database/Database+LastInsertRowidTests.swift b/Tests/LSQLiteTests/Database/Database+LastInsertRowidTests.swift new file mode 100644 index 0000000..a862a1a --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+LastInsertRowidTests.swift @@ -0,0 +1,30 @@ +import LSQLite +import Testing + +@Suite("Database+LastInsertRowid") +final class DatabaseLastInsertRowidTests { + private let database: Database + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + self.database = openDatabase + try #require(openDatabase.exec("CREATE TABLE items(id INTEGER PRIMARY KEY)") == .ok) + } + + deinit { + _ = database.close() + } + + @Test("lastInsertedRowID and setLastInsertedRowID") + func lastInsertedRowIDAndSetLastInsertedRowID() throws { + try #require(database.exec("INSERT INTO items DEFAULT VALUES") == .ok) + let inserted = database.lastInsertedRowID() + #expect(inserted.rawValue > 0) + + let override = RowID(rawValue: 99) + database.setLastInsertedRowID(override) + #expect(database.lastInsertedRowID().rawValue == 99) + } +} diff --git a/Tests/LSQLiteTests/Database/Database+LimitTests.swift b/Tests/LSQLiteTests/Database/Database+LimitTests.swift new file mode 100644 index 0000000..9d11edc --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+LimitTests.swift @@ -0,0 +1,68 @@ +import LSQLite +import MissedSwiftSQLite +import Testing + +@Suite("Database+Limit") +final class DatabaseLimitCategoryRawValueTests { + private let database: Database + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + self.database = try #require(database) + } + + deinit { + _ = database.close() + } + + @Test("init(rawValue:) preserves rawValue") + func rawValueRoundTrip() { + let rawValue = Int32(5) + let category = Database.LimitCategory(rawValue: rawValue) + #expect(category.rawValue == rawValue) + } + + @Test("LimitCategory constants match SQLite") + func limitCategoryConstantsMatchSQLite() { + #expect(Database.LimitCategory.length.rawValue == SQLITE_LIMIT_LENGTH) + #expect(Database.LimitCategory.sqlLength.rawValue == SQLITE_LIMIT_SQL_LENGTH) + #expect(Database.LimitCategory.column.rawValue == SQLITE_LIMIT_COLUMN) + #expect(Database.LimitCategory.exprDepth.rawValue == SQLITE_LIMIT_EXPR_DEPTH) + #expect(Database.LimitCategory.compoundSelect.rawValue == SQLITE_LIMIT_COMPOUND_SELECT) + #expect(Database.LimitCategory.vdbeOp.rawValue == SQLITE_LIMIT_VDBE_OP) + #expect(Database.LimitCategory.functionArg.rawValue == SQLITE_LIMIT_FUNCTION_ARG) + #expect(Database.LimitCategory.attached.rawValue == SQLITE_LIMIT_ATTACHED) + #expect(Database.LimitCategory.likePatternLength.rawValue == SQLITE_LIMIT_LIKE_PATTERN_LENGTH) + #expect(Database.LimitCategory.variableNumber.rawValue == SQLITE_LIMIT_VARIABLE_NUMBER) + #expect(Database.LimitCategory.triggerDepth.rawValue == SQLITE_LIMIT_TRIGGER_DEPTH) + #expect(Database.LimitCategory.workerThreads.rawValue == SQLITE_LIMIT_WORKER_THREADS) + } + + @Test("LimitCategory descriptions map values") + func limitCategoryDescriptions() { + #expect(Database.LimitCategory.length.description == "length") + #expect(Database.LimitCategory.sqlLength.description == "sql length") + #expect(Database.LimitCategory.column.description == "column") + #expect(Database.LimitCategory.exprDepth.description == "expression depth") + #expect(Database.LimitCategory.compoundSelect.description == "compound select") + #expect(Database.LimitCategory.vdbeOp.description == "vdbe op") + #expect(Database.LimitCategory.functionArg.description == "function arg") + #expect(Database.LimitCategory.attached.description == "attached") + #expect(Database.LimitCategory.likePatternLength.description == "like pattern length") + #expect(Database.LimitCategory.variableNumber.description == "variable number") + #expect(Database.LimitCategory.triggerDepth.description == "trigger depth") + #expect(Database.LimitCategory.workerThreads.description == "worker threads") + #expect(Database.LimitCategory(rawValue: -3).description == "unknown") + #expect(Database.LimitCategory.length.debugDescription == "SQLITE_LIMIT_LENGTH") + #expect(Database.LimitCategory(rawValue: -3).debugDescription == "-3") + } + + @Test("limit and setLimit round-trip values") + func limitAndSetLimitRoundTripValues() { + let current = database.limit(for: .length) + let previous = database.setLimit(current, for: .length) + #expect(previous == current) + #expect(database.limit(for: .length) == current) + } +} diff --git a/Tests/LSQLiteTests/Database/Database+OpenTests.swift b/Tests/LSQLiteTests/Database/Database+OpenTests.swift new file mode 100644 index 0000000..7e09a3a --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+OpenTests.swift @@ -0,0 +1,85 @@ +import LSQLite +import MissedSwiftSQLite +import Testing + +@Suite("Database+Open") +struct DatabaseOpenTests { + @Test("FileName init(rawValue:) preserves rawValue") + func fileNameRawValueRoundTrip() { + let rawValue = "test.db" + let fileName = Database.FileName(rawValue: rawValue) + #expect(fileName.rawValue == rawValue) + } + + @Test("OpenFlag init(rawValue:) preserves rawValue") + func openFlagRawValueRoundTrip() { + let rawValue = Int32(0x1200) + let openFlag = Database.OpenFlag(rawValue: rawValue) + #expect(openFlag.rawValue == rawValue) + } + + @Test("OpenFlag init(rawValue:) preserves combined rawValue") + func openFlagCombinedRawValueRoundTrip() { + let rawValue = Database.OpenFlag.readwrite.rawValue | Database.OpenFlag.create.rawValue + let openFlag = Database.OpenFlag(rawValue: rawValue) + #expect(openFlag.rawValue == rawValue) + } + + @Test("OpenFlag constants match SQLite") + func openFlagConstantsMatchSQLite() { + #expect(Database.OpenFlag.readonly.rawValue == SQLITE_OPEN_READONLY) + #expect(Database.OpenFlag.readwrite.rawValue == SQLITE_OPEN_READWRITE) + #expect(Database.OpenFlag.create.rawValue == SQLITE_OPEN_CREATE) + #expect(Database.OpenFlag.deleteOnClose.rawValue == SQLITE_OPEN_DELETEONCLOSE) + #expect(Database.OpenFlag.exclusive.rawValue == SQLITE_OPEN_EXCLUSIVE) + #expect(Database.OpenFlag.autoproxy.rawValue == SQLITE_OPEN_AUTOPROXY) + #expect(Database.OpenFlag.uri.rawValue == SQLITE_OPEN_URI) + #expect(Database.OpenFlag.memory.rawValue == SQLITE_OPEN_MEMORY) + #expect(Database.OpenFlag.mainDB.rawValue == SQLITE_OPEN_MAIN_DB) + #expect(Database.OpenFlag.tempDB.rawValue == SQLITE_OPEN_TEMP_DB) + #expect(Database.OpenFlag.transientDB.rawValue == SQLITE_OPEN_TRANSIENT_DB) + #expect(Database.OpenFlag.mainJournal.rawValue == SQLITE_OPEN_MAIN_JOURNAL) + #expect(Database.OpenFlag.tempJournal.rawValue == SQLITE_OPEN_TEMP_JOURNAL) + #expect(Database.OpenFlag.subjournal.rawValue == SQLITE_OPEN_SUBJOURNAL) + #expect(Database.OpenFlag.masterJournal.rawValue == SQLITE_OPEN_MASTER_JOURNAL) + #expect(Database.OpenFlag.noMutex.rawValue == SQLITE_OPEN_NOMUTEX) + #expect(Database.OpenFlag.fullMutex.rawValue == SQLITE_OPEN_FULLMUTEX) + #expect(Database.OpenFlag.sharedCache.rawValue == SQLITE_OPEN_SHAREDCACHE) + #expect(Database.OpenFlag.privateCache.rawValue == SQLITE_OPEN_PRIVATECACHE) + #expect(Database.OpenFlag.wal.rawValue == SQLITE_OPEN_WAL) +#if canImport(Darwin) + #expect(Database.OpenFlag.fileProtectionComplete.rawValue == SQLITE_OPEN_FILEPROTECTION_COMPLETE) + #expect(Database.OpenFlag.fileProtectionCompleteUnlessOpen.rawValue == SQLITE_OPEN_FILEPROTECTION_COMPLETEUNLESSOPEN) + #expect(Database.OpenFlag.fileProtectionCompleteUntilFirstUserAuthentication.rawValue == SQLITE_OPEN_FILEPROTECTION_COMPLETEUNTILFIRSTUSERAUTHENTICATION) + #expect(Database.OpenFlag.fileProtectionNone.rawValue == SQLITE_OPEN_FILEPROTECTION_NONE) + #expect(Database.OpenFlag.fileProtectionMask.rawValue == SQLITE_OPEN_FILEPROTECTION_MASK) +#endif + } + + @Test("FileName description reflects rawValue") + func fileNameDescriptionReflectsRawValue() { + #expect(Database.FileName.memory.description == ":memory:") + #expect(Database.FileName.temporary.description == "") + #expect(Database.FileName(rawValue: "custom.db").description == "custom.db") + } + + @Test("OpenFlag descriptions map values") + func openFlagDescriptions() { + #expect(Database.OpenFlag([]).description == "[]") + #expect(Database.OpenFlag.readwrite.description.contains(".readwrite")) + #expect(Database.OpenFlag(rawValue: 0x4000_0000).description == "unknown") + let mixed = Database.OpenFlag(rawValue: Database.OpenFlag.readonly.rawValue | 0x4000_0000) + #expect(mixed.description.contains("unknown")) + #expect(Database.OpenFlag.readonly.debugDescription.contains("SQLITE_OPEN_READONLY")) + #expect(mixed.debugDescription.contains("0x")) + #expect(Database.OpenFlag(rawValue: 0x4000_0000).debugDescription.hasPrefix("0x")) + } + + @Test("open creates an in-memory database") + func openCreatesInMemoryDatabase() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + #expect(openDatabase.close() == .ok) + } +} diff --git a/Tests/LSQLiteTests/Database/Database+ProgressHandlerTests.swift b/Tests/LSQLiteTests/Database/Database+ProgressHandlerTests.swift new file mode 100644 index 0000000..2d5c711 --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+ProgressHandlerTests.swift @@ -0,0 +1,43 @@ +import LSQLite +import Testing + +@Suite("Database+ProgressHandler") +final class DatabaseProgressHandlerResultRawValueTests { + private let database: Database + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + self.database = try #require(database) + } + + deinit { + _ = database.close() + } + + @Test("init(rawValue:) preserves rawValue") + func rawValueRoundTrip() { + let rawValue = Int32(8) + let result = Database.ProgressHandlerResult(rawValue: rawValue) + #expect(result.rawValue == rawValue) + } + + @Test("ProgressHandlerResult descriptions map values") + func progressHandlerResultDescriptions() { + #expect(Database.ProgressHandlerResult.continue.description == "continue") + #expect(Database.ProgressHandlerResult.interrupt.description == "interrupt") + #expect(Database.ProgressHandlerResult(rawValue: 2).description == "unknown") + #expect(Database.ProgressHandlerResult.continue.debugDescription == "continue (0)") + #expect(Database.ProgressHandlerResult(rawValue: 2).debugDescription == "unknown (2)") + } + + @Test("setProgressHandler registers handler") + func setProgressHandlerRegistersHandler() { + database.setProgressHandler(instructionCount: 10, handler: progressHandler) + database.setProgressHandler(instructionCount: 0, handler: nil) + } +} + +private func progressHandler(_ userData: UnsafeMutableRawPointer?) -> Int32 { + Database.ProgressHandlerResult.continue.rawValue +} diff --git a/Tests/LSQLiteTests/Database/Database+ReadonlyTests.swift b/Tests/LSQLiteTests/Database/Database+ReadonlyTests.swift new file mode 100644 index 0000000..cdf47fe --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+ReadonlyTests.swift @@ -0,0 +1,40 @@ +import LSQLite +import Testing + +@Suite("Database+Readonly") +final class DatabaseReadWriteAccessStateRawValueTests { + private let database: Database + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + self.database = try #require(database) + } + + deinit { + _ = database.close() + } + + @Test("init(rawValue:) preserves rawValue") + func rawValueRoundTrip() { + let rawValue = Int32(-7) + let state = Database.ReadWriteAccessState(rawValue: rawValue) + #expect(state.rawValue == rawValue) + } + + @Test("ReadWriteAccessState descriptions map values") + func readWriteAccessStateDescriptions() { + #expect(Database.ReadWriteAccessState.noDatabase.description == "no database") + #expect(Database.ReadWriteAccessState.readwrite.description == "readwrite") + #expect(Database.ReadWriteAccessState.readonly.description == "readonly") + #expect(Database.ReadWriteAccessState(rawValue: 9).description == "unknown") + #expect(Database.ReadWriteAccessState.readwrite.debugDescription == "readwrite (0)") + #expect(Database.ReadWriteAccessState(rawValue: 9).debugDescription == "unknown (9)") + } + + @Test("readWriteAccessState returns status for named database") + func readWriteAccessStateReturnsStatusForNamedDatabase() { + #expect(database.readWriteAccessState(forDatabaseNamed: "main") == .readwrite) + #expect(database.readWriteAccessState(forDatabaseNamed: "missing") == .noDatabase) + } +} diff --git a/Tests/LSQLiteTests/Database/Database+StatementTests.swift b/Tests/LSQLiteTests/Database/Database+StatementTests.swift new file mode 100644 index 0000000..b23c3a7 --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+StatementTests.swift @@ -0,0 +1,40 @@ +import LSQLite +import Testing + +@Suite("Database+Statement") +final class DatabaseStatementTests { + private let database: Database + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + self.database = try #require(database) + } + + deinit { + _ = database.close() + } + + @Test("nextStatement iterates prepared statements") + func nextStatementIteratesPreparedStatements() throws { + var first: Statement? + var second: Statement? + try #require(Statement.prepare(&first, sql: "SELECT 1", for: database) == .ok) + try #require(Statement.prepare(&second, sql: "SELECT 2", for: database) == .ok) + let firstStatement = try #require(first) + let secondStatement = try #require(second) + + let firstFromAPI = try #require(database.nextStatement(after: nil)) + let secondFromAPI = database.nextStatement(after: firstFromAPI) + let thirdFromAPI = database.nextStatement(after: secondFromAPI) + + let preparedPointers = [firstStatement.rawValue, secondStatement.rawValue] + #expect(preparedPointers.contains(firstFromAPI.rawValue)) + #expect(preparedPointers.contains(try #require(secondFromAPI).rawValue)) + #expect(firstFromAPI.rawValue != secondFromAPI?.rawValue) + #expect(thirdFromAPI == nil) + + #expect(firstStatement.finalize() == .ok) + #expect(secondStatement.finalize() == .ok) + } +} diff --git a/Tests/LSQLiteTests/Database/Database+TraceTests.swift b/Tests/LSQLiteTests/Database/Database+TraceTests.swift new file mode 100644 index 0000000..e40bbd9 --- /dev/null +++ b/Tests/LSQLiteTests/Database/Database+TraceTests.swift @@ -0,0 +1,102 @@ +import LSQLite +import MissedSwiftSQLite +import Testing + +@Suite("Database+Trace") +struct DatabaseTraceTests { + @Test("TraceEventCallbackResult init(rawValue:) preserves rawValue") + func traceEventCallbackResultRawValueRoundTrip() { + guard #available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *) else { + return + } + let rawValue = Int32(12) + let result = Database.TraceEventCallbackResult(rawValue: rawValue) + #expect(result.rawValue == rawValue) + } + + @Test("TraceEventCode init(rawValue:) preserves rawValue") + func traceEventCodeRawValueRoundTrip() { + guard #available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *) else { + return + } + let rawValue = UInt32(0x20) + let code = Database.TraceEventCode(rawValue: rawValue) + #expect(code.rawValue == rawValue) + } + + @Test("TraceEventCode init(rawValue:) preserves combined rawValue") + func traceEventCodeCombinedRawValueRoundTrip() { + guard #available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *) else { + return + } + let rawValue = Database.TraceEventCode.statement.rawValue | Database.TraceEventCode.profile.rawValue + let code = Database.TraceEventCode(rawValue: rawValue) + #expect(code.rawValue == rawValue) + } + + @Test("TraceEventCode constants match SQLite") + func traceEventCodeConstantsMatchSQLite() { + guard #available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *) else { + return + } + #expect(Database.TraceEventCode.statement.rawValue == UInt32(SQLITE_TRACE_STMT)) + #expect(Database.TraceEventCode.profile.rawValue == UInt32(SQLITE_TRACE_PROFILE)) + #expect(Database.TraceEventCode.row.rawValue == UInt32(SQLITE_TRACE_ROW)) + #expect(Database.TraceEventCode.close.rawValue == UInt32(SQLITE_TRACE_CLOSE)) + } + + @Test("Trace event descriptions map values") + func traceEventDescriptionsMapValues() { + guard #available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *) else { + return + } + #expect(Database.TraceEventCallbackResult.ok.description == "ok") + #expect(Database.TraceEventCallbackResult(rawValue: 9).description == "unknown") + #expect(Database.TraceEventCallbackResult.ok.debugDescription == "ok (0)") + #expect(Database.TraceEventCode([]).description == "[]") + #expect(Database.TraceEventCode.statement.description.contains(".statement")) + #expect(Database.TraceEventCode(rawValue: 0x80).description == "unknown") + let mixed = Database.TraceEventCode(rawValue: Database.TraceEventCode.row.rawValue | 0x80) + #expect(mixed.description.contains("unknown")) + #expect(Database.TraceEventCode.statement.debugDescription.contains("SQLITE_TRACE_STMT")) + #expect(mixed.debugDescription.contains("0x")) + #expect(Database.TraceEventCode(rawValue: 0x80).debugDescription.hasPrefix("0x")) + } + + @Test("setTraceCallback registers callback") + func setTraceCallbackRegistersCallback() throws { + guard #available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *) else { + return + } + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + var probe = TraceProbe() + #expect(openDatabase.setTraceCallback(for: [.statement], userData: &probe, callback: traceCallback) == .ok) + + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT 1", for: openDatabase) == .ok) + let prepared = try #require(statement) + #expect(prepared.step() == .row) + #expect(prepared.step() == .done) + #expect(prepared.finalize() == .ok) + + #expect(probe.called) + #expect(openDatabase.setTraceCallback(for: [], userData: nil, callback: nil) == .ok) + _ = openDatabase.close() + } +} + +private struct TraceProbe { + var called = false +} + +@available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *) +private func traceCallback(_ traceEventCode: UInt32, _ userData: UnsafeMutableRawPointer?, _ p: UnsafeMutableRawPointer?, _ x: UnsafeMutableRawPointer?) -> Int32 { + guard let userData else { + return Database.TraceEventCallbackResult.ok.rawValue + } + let probe = userData.assumingMemoryBound(to: TraceProbe.self) + probe.pointee.called = true + return Database.TraceEventCallbackResult.ok.rawValue +} diff --git a/Tests/LSQLiteTests/Database/DatabaseTests.swift b/Tests/LSQLiteTests/Database/DatabaseTests.swift new file mode 100644 index 0000000..5bd2bd8 --- /dev/null +++ b/Tests/LSQLiteTests/Database/DatabaseTests.swift @@ -0,0 +1,12 @@ +import LSQLite +import Testing + +@Suite("Database") +struct DatabaseRawValueTests { + @Test("init(rawValue:) preserves rawValue") + func rawValueRoundTrip() { + let rawValue = OpaquePointer(bitPattern: 0x1)! + let database = Database(rawValue: rawValue) + #expect(database.rawValue == rawValue) + } +} diff --git a/Tests/LSQLiteTests/DatabaseFileNameTests.swift b/Tests/LSQLiteTests/DatabaseFileNameTests.swift deleted file mode 100644 index 408d808..0000000 --- a/Tests/LSQLiteTests/DatabaseFileNameTests.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Testing -import LSQLite - -@Suite -struct DatabaseFileNameTests { - @Test - func constantsHaveExpectedRawValues() { - #expect(Database.FileName.memory.rawValue == ":memory:") - #expect(Database.FileName.temporary.rawValue == "") - } - - @Test - func uriFilenamesMatchSQLiteExamples() { - #expect(Database.FileName.uri(path: .init(rawValue: "data.db")).rawValue == "file:data.db") - #expect( - Database.FileName.uri(authority: .empty, path: .init(rawValue: "/home/fred/data.db")).rawValue - == "file:///home/fred/data.db" - ) - #expect( - Database.FileName.uri(authority: .localhost, path: .init(rawValue: "/home/fred/data.db")).rawValue - == "file://localhost/home/fred/data.db" - ) - #expect( - Database.FileName.uri(authority: .localhost, path: .init(rawValue: "home/fred/data.db")).rawValue - == "file://localhost/home/fred/data.db" - ) - #expect( - Database.FileName.uri( - path: .init(rawValue: "data.db"), - query: .init([.mode(.ro), .cache(.private)]) - ).rawValue == "file:data.db?mode=ro&cache=private" - ) - } - - @Test - func uriComponentInitializersPercentEncodeReservedDelimiters() { - #expect(Database.FileName.URI.Path("data?.db#x%y").rawValue == "data%3F.db%23x%25y") - #expect(Database.FileName.URI.QueryKey("a&b=c#d%").rawValue == "a%26b%3Dc%23d%25") - #expect(Database.FileName.URI.QueryValue("a&b=c#d%").rawValue == "a%26b%3Dc%23d%25") - #expect(Database.FileName.URI.Fragment("a%b").rawValue == "a%25b") - } -} diff --git a/Tests/LSQLiteTests/DatatypeTests.swift b/Tests/LSQLiteTests/DatatypeTests.swift new file mode 100644 index 0000000..2dadc23 --- /dev/null +++ b/Tests/LSQLiteTests/DatatypeTests.swift @@ -0,0 +1,47 @@ +import LSQLite +import MissedSwiftSQLite +import Testing + +@Suite("Datatype") +struct DatatypeRawValueTests { + @Test("init(rawValue:) preserves rawValue") + func rawValueRoundTrip() { + let rawValue = Int32(37) + let datatype = Datatype(rawValue: rawValue) + #expect(datatype.rawValue == rawValue) + } + + @Test("static constants match SQLite") + func staticConstantsMatchSQLite() { + #expect(Datatype.integer.rawValue == SQLITE_INTEGER) + #expect(Datatype.float.rawValue == SQLITE_FLOAT) + #expect(Datatype.blob.rawValue == SQLITE_BLOB) + #expect(Datatype.null.rawValue == SQLITE_NULL) + #expect(Datatype.text.rawValue == SQLITE_TEXT) + } + + @Test("description maps known values") + func descriptionMapsKnownValues() { + #expect(Datatype.integer.description == "integer") + #expect(Datatype.float.description == "float") + #expect(Datatype.blob.description == "blob") + #expect(Datatype.null.description == "null") + #expect(Datatype.text.description == "text") + } + + @Test("debugDescription maps known values") + func debugDescriptionMapsKnownValues() { + #expect(Datatype.integer.debugDescription == "SQLITE_INTEGER") + #expect(Datatype.float.debugDescription == "SQLITE_FLOAT") + #expect(Datatype.blob.debugDescription == "SQLITE_BLOB") + #expect(Datatype.null.debugDescription == "SQLITE_NULL") + #expect(Datatype.text.debugDescription == "SQLITE_TEXT") + } + + @Test("unknown values use fallback strings") + func unknownValuesUseFallbackStrings() { + let unknown = Datatype(rawValue: 1234) + #expect(unknown.description == "unknown") + #expect(unknown.debugDescription == "1234") + } +} diff --git a/Tests/LSQLiteTests/Error.swift b/Tests/LSQLiteTests/Error.swift deleted file mode 100644 index f1d5d4c..0000000 --- a/Tests/LSQLiteTests/Error.swift +++ /dev/null @@ -1,6 +0,0 @@ -import LSQLite - -enum Error: Swift.Error { - case result(ResultCode) - case unknown -} diff --git a/Tests/LSQLiteTests/ExecTests.swift b/Tests/LSQLiteTests/ExecTests.swift deleted file mode 100644 index 69d98df..0000000 --- a/Tests/LSQLiteTests/ExecTests.swift +++ /dev/null @@ -1,73 +0,0 @@ -import XCTest -import LSQLite - -class ExecTests: XCTestCase { - - var database: Database! - - override func setUpWithError() throws { - try super.setUpWithError() - database = try { - var database: Database? = nil - guard Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create, .memory]) == .ok else { - if let database = database { - throw Error.result(database.lastErrorCode) - } else { - throw Error.unknown - } - } - return database - }() - } - - override func tearDownWithError() throws { - try super.tearDownWithError() - guard database.close() == .ok else { - throw Error.result(database.lastErrorCode) - } - } - - func testCreateDatabaseInsertRowRowsAndGetThemBack() { - XCTAssertEqual(database.exec("CREATE TABLE t(num INTEGER, txt TEXT);"), .ok) - XCTAssertEqual(database.exec("INSERT INTO t VALUES(1, 'text1');"), .ok) - XCTAssertEqual(database.exec("INSERT INTO t VALUES(2, NULL);"), .ok) - let counter = RefWrappedValue(0) - let resultCode = database.exec("SELECT num, txt FROM t;", userData: Unmanaged.passUnretained(counter).toOpaque()) { userData, count, values, columns in - let counter = Unmanaged>.fromOpaque(userData!).takeUnretainedValue() - counter.value += 1 - let mappedValues: [String: String] = { - var typedValues: [String?] = [] - var typedColumns: [String] = [] - for i in 0...stride)) - guard let valuePointer = aggregatePointer?.assumingMemoryBound(to: Int32.self) else { - context.resultNoMemoryError() - return - } - valuePointer.pointee += 1 - }, finalHandler: { context in - let context = Context(rawValue: context!) - let value = context.aggregateContext(size: Int32(MemoryLayout.stride))!.assumingMemoryBound(to: Int32.self).pointee - context.resultInt(value) - }) - XCTAssertEqual(myCountResultCode, .ok) - } - - override func tearDownWithError() throws { - try super.tearDownWithError() - guard database.close() == .ok else { - throw Error.result(database.lastErrorCode) - } - } - - func testCreateDatabaseInsertRowRowsAndGetThemBack() throws { - let createTableStatement: Statement = try { - var statement: Statement? = nil - let resultCode = Statement.prepare(&statement, sql: "CREATE TABLE t(num INTEGER);", for: database) - guard resultCode == .ok, statement != nil else { - throw Error.result(resultCode) - } - return statement! - }() - XCTAssertEqual(createTableStatement.step(), .done) - XCTAssertEqual(createTableStatement.finalize(), .ok) - - let insertStatement: Statement = try { - var statement: Statement? = nil - let resultCode = Statement.prepare(&statement, sql: "INSERT INTO t VALUES(?);", for: database) - guard resultCode == .ok, statement != nil else { - throw Error.result(resultCode) - } - return statement! - }() - - XCTAssertEqual(insertStatement.bindInt(1, at: 1), .ok) - XCTAssertEqual(insertStatement.step(), .done) - XCTAssertEqual(insertStatement.clearBindings(), .ok) - XCTAssertEqual(insertStatement.reset(), .ok) - - XCTAssertEqual(insertStatement.bindInt(2, at: 1), .ok) - XCTAssertEqual(insertStatement.step(), .done) - XCTAssertEqual(insertStatement.clearBindings(), .ok) - XCTAssertEqual(insertStatement.reset(), .ok) - - XCTAssertEqual(insertStatement.finalize(), .ok) - - let selectStatement: Statement = try { - var statement: Statement? = nil - let resultCode = Statement.prepare(&statement, sql: "SELECT num, my_five() as nop FROM t;", for: database) - guard resultCode == .ok, statement != nil else { - throw Error.result(resultCode) - } - return statement! - }() - - XCTAssertEqual(selectStatement.step(), .row) - XCTAssertEqual(selectStatement.columnInt(at: 0), 1) - XCTAssertEqual(selectStatement.columnInt(at: 1), 5) - - XCTAssertEqual(selectStatement.step(), .row) - XCTAssertEqual(selectStatement.columnInt(at: 0), 2) - XCTAssertEqual(selectStatement.columnInt(at: 1), 5) - - XCTAssertEqual(selectStatement.step(), .done) - - XCTAssertEqual(selectStatement.finalize(), .ok) - - let countStatement: Statement = try { - var statement: Statement? = nil - let resultCode = Statement.prepare(&statement, sql: "SELECT my_count() as count FROM t;", for: database) - guard resultCode == .ok, statement != nil else { - throw Error.result(resultCode) - } - return statement! - }() - - XCTAssertEqual(countStatement.step(), .row) - XCTAssertEqual(countStatement.columnInt(at: 0), 2) - - XCTAssertEqual(countStatement.step(), .done) - - XCTAssertEqual(countStatement.finalize(), .ok) - } - -} diff --git a/Tests/LSQLiteTests/RefWrappedValue.swift b/Tests/LSQLiteTests/RefWrappedValue.swift deleted file mode 100644 index f87192c..0000000 --- a/Tests/LSQLiteTests/RefWrappedValue.swift +++ /dev/null @@ -1,6 +0,0 @@ -class RefWrappedValue { - var value: Value - init(_ value: Value) { - self.value = value - } -} diff --git a/Tests/LSQLiteTests/ResultCodeTests.swift b/Tests/LSQLiteTests/ResultCodeTests.swift new file mode 100644 index 0000000..899c386 --- /dev/null +++ b/Tests/LSQLiteTests/ResultCodeTests.swift @@ -0,0 +1,242 @@ +import LSQLite +import MissedSwiftSQLite +import Testing + +@Suite("ResultCode") +struct ResultCodeRawValueTests { + @Test("init(rawValue:) preserves rawValue") + func rawValueRoundTrip() { + let rawValue = Int32(123) + let resultCode = ResultCode(rawValue: rawValue) + #expect(resultCode.rawValue == rawValue) + } + + @Test("static result codes match SQLite") + func staticResultCodesMatchSQLite() { + #expect(ResultCode.ok.rawValue == SQLITE_OK) + #expect(ResultCode.error.rawValue == SQLITE_ERROR) + #expect(ResultCode.`internal`.rawValue == SQLITE_INTERNAL) + #expect(ResultCode.permission.rawValue == SQLITE_PERM) + #expect(ResultCode.abort.rawValue == SQLITE_ABORT) + #expect(ResultCode.busy.rawValue == SQLITE_BUSY) + #expect(ResultCode.locked.rawValue == SQLITE_LOCKED) + #expect(ResultCode.noMemory.rawValue == SQLITE_NOMEM) + #expect(ResultCode.readonly.rawValue == SQLITE_READONLY) + #expect(ResultCode.interrupt.rawValue == SQLITE_INTERRUPT) + #expect(ResultCode.ioError.rawValue == SQLITE_IOERR) + #expect(ResultCode.corrupt.rawValue == SQLITE_CORRUPT) + #expect(ResultCode.notFound.rawValue == SQLITE_NOTFOUND) + #expect(ResultCode.full.rawValue == SQLITE_FULL) + #expect(ResultCode.cantOpen.rawValue == SQLITE_CANTOPEN) + #expect(ResultCode.`protocol`.rawValue == SQLITE_PROTOCOL) + #expect(ResultCode.empty.rawValue == SQLITE_EMPTY) + #expect(ResultCode.schema.rawValue == SQLITE_SCHEMA) + #expect(ResultCode.tooBig.rawValue == SQLITE_TOOBIG) + #expect(ResultCode.constraint.rawValue == SQLITE_CONSTRAINT) + #expect(ResultCode.mismatch.rawValue == SQLITE_MISMATCH) + #expect(ResultCode.misuse.rawValue == SQLITE_MISUSE) + #expect(ResultCode.noLFS.rawValue == SQLITE_NOLFS) + #expect(ResultCode.auth.rawValue == SQLITE_AUTH) + #expect(ResultCode.format.rawValue == SQLITE_FORMAT) + #expect(ResultCode.range.rawValue == SQLITE_RANGE) + #expect(ResultCode.notADB.rawValue == SQLITE_NOTADB) + #expect(ResultCode.notice.rawValue == SQLITE_NOTICE) + #expect(ResultCode.warning.rawValue == SQLITE_WARNING) + #expect(ResultCode.row.rawValue == SQLITE_ROW) + #expect(ResultCode.done.rawValue == SQLITE_DONE) + } + + @Test("extended result codes match SQLite") + func extendedResultCodesMatchSQLite() { + #expect(ResultCode.errorMissingCollSeq.rawValue == LSQLITE_ERROR_MISSING_COLLSEQ) + #expect(ResultCode.errorRetry.rawValue == LSQLITE_ERROR_RETRY) + #expect(ResultCode.errorSnapshot.rawValue == LSQLITE_ERROR_SNAPSHOT) + #expect(ResultCode.ioErrorRead.rawValue == LSQLITE_IOERR_READ) + #expect(ResultCode.ioErrorShortRead.rawValue == LSQLITE_IOERR_SHORT_READ) + #expect(ResultCode.ioErrorWrite.rawValue == LSQLITE_IOERR_WRITE) + #expect(ResultCode.ioErrorFSync.rawValue == LSQLITE_IOERR_FSYNC) + #expect(ResultCode.ioErrorDirFSync.rawValue == LSQLITE_IOERR_DIR_FSYNC) + #expect(ResultCode.ioErrorTruncate.rawValue == LSQLITE_IOERR_TRUNCATE) + #expect(ResultCode.ioErrorFStat.rawValue == LSQLITE_IOERR_FSTAT) + #expect(ResultCode.ioErrorUnlock.rawValue == LSQLITE_IOERR_UNLOCK) + #expect(ResultCode.ioErrorRDLock.rawValue == LSQLITE_IOERR_RDLOCK) + #expect(ResultCode.ioErrorDelete.rawValue == LSQLITE_IOERR_DELETE) + #expect(ResultCode.ioErrorBlocked.rawValue == LSQLITE_IOERR_BLOCKED) + #expect(ResultCode.ioErrorNoMem.rawValue == LSQLITE_IOERR_NOMEM) + #expect(ResultCode.ioErrorAccess.rawValue == LSQLITE_IOERR_ACCESS) + #expect(ResultCode.ioErrorCheckReservedLock.rawValue == LSQLITE_IOERR_CHECKRESERVEDLOCK) + #expect(ResultCode.ioErrorLock.rawValue == LSQLITE_IOERR_LOCK) + #expect(ResultCode.ioErrorClose.rawValue == LSQLITE_IOERR_CLOSE) + #expect(ResultCode.ioErrorDirClose.rawValue == LSQLITE_IOERR_DIR_CLOSE) + #expect(ResultCode.ioErrorShMOpen.rawValue == LSQLITE_IOERR_SHMOPEN) + #expect(ResultCode.ioErrorShMSize.rawValue == LSQLITE_IOERR_SHMSIZE) + #expect(ResultCode.ioErrorShMLock.rawValue == LSQLITE_IOERR_SHMLOCK) + #expect(ResultCode.ioErrorShMMap.rawValue == LSQLITE_IOERR_SHMMAP) + #expect(ResultCode.ioErrorSeek.rawValue == LSQLITE_IOERR_SEEK) + #expect(ResultCode.ioErrorDeleteNoEnt.rawValue == LSQLITE_IOERR_DELETE_NOENT) + #expect(ResultCode.ioErrorMMap.rawValue == LSQLITE_IOERR_MMAP) + #expect(ResultCode.ioErrorGetTempPath.rawValue == LSQLITE_IOERR_GETTEMPPATH) + #expect(ResultCode.ioErrorConvPath.rawValue == LSQLITE_IOERR_CONVPATH) + #expect(ResultCode.ioErrorVNode.rawValue == LSQLITE_IOERR_VNODE) + #expect(ResultCode.ioErrorAuth.rawValue == LSQLITE_IOERR_AUTH) + #expect(ResultCode.ioErrorBeginAtomic.rawValue == LSQLITE_IOERR_BEGIN_ATOMIC) + #expect(ResultCode.ioErrorCommitAtomic.rawValue == LSQLITE_IOERR_COMMIT_ATOMIC) + #expect(ResultCode.ioErrorRollbackAtomic.rawValue == LSQLITE_IOERR_ROLLBACK_ATOMIC) + #expect(ResultCode.lockedSharedCache.rawValue == LSQLITE_LOCKED_SHAREDCACHE) + #expect(ResultCode.lockedVTab.rawValue == LSQLITE_LOCKED_VTAB) + #expect(ResultCode.busyRecovery.rawValue == LSQLITE_BUSY_RECOVERY) + #expect(ResultCode.busySnapshot.rawValue == LSQLITE_BUSY_SNAPSHOT) + #expect(ResultCode.cantOpenNoTempDir.rawValue == LSQLITE_CANTOPEN_NOTEMPDIR) + #expect(ResultCode.cantOpenIsDir.rawValue == LSQLITE_CANTOPEN_ISDIR) + #expect(ResultCode.cantOpenFullPath.rawValue == LSQLITE_CANTOPEN_FULLPATH) + #expect(ResultCode.cantOpenConvPath.rawValue == LSQLITE_CANTOPEN_CONVPATH) + #expect(ResultCode.cantOpenDirtyWAL.rawValue == LSQLITE_CANTOPEN_DIRTYWAL) + #expect(ResultCode.corruptVTab.rawValue == LSQLITE_CORRUPT_VTAB) + #expect(ResultCode.corruptSequence.rawValue == LSQLITE_CORRUPT_SEQUENCE) + #expect(ResultCode.readonlyRecovery.rawValue == LSQLITE_READONLY_RECOVERY) + #expect(ResultCode.readonlyCantLock.rawValue == LSQLITE_READONLY_CANTLOCK) + #expect(ResultCode.readonlyRollback.rawValue == LSQLITE_READONLY_ROLLBACK) + #expect(ResultCode.readonlyDBMoved.rawValue == LSQLITE_READONLY_DBMOVED) + #expect(ResultCode.readonlyCantInit.rawValue == LSQLITE_READONLY_CANTINIT) + #expect(ResultCode.readonlyDirectory.rawValue == LSQLITE_READONLY_DIRECTORY) + #expect(ResultCode.abortRollback.rawValue == LSQLITE_ABORT_ROLLBACK) + #expect(ResultCode.constraintCheck.rawValue == LSQLITE_CONSTRAINT_CHECK) + #expect(ResultCode.constraintCommitHook.rawValue == LSQLITE_CONSTRAINT_COMMITHOOK) + #expect(ResultCode.constraintForeignKey.rawValue == LSQLITE_CONSTRAINT_FOREIGNKEY) + #expect(ResultCode.constraintFunction.rawValue == LSQLITE_CONSTRAINT_FUNCTION) + #expect(ResultCode.constraintNotNull.rawValue == LSQLITE_CONSTRAINT_NOTNULL) + #expect(ResultCode.constraintPrimaryKey.rawValue == LSQLITE_CONSTRAINT_PRIMARYKEY) + #expect(ResultCode.constraintTrigger.rawValue == LSQLITE_CONSTRAINT_TRIGGER) + #expect(ResultCode.constraintUnique.rawValue == LSQLITE_CONSTRAINT_UNIQUE) + #expect(ResultCode.constraintVTab.rawValue == LSQLITE_CONSTRAINT_VTAB) + #expect(ResultCode.constraintRowID.rawValue == LSQLITE_CONSTRAINT_ROWID) + #expect(ResultCode.noticeRecoverWAL.rawValue == LSQLITE_NOTICE_RECOVER_WAL) + #expect(ResultCode.noticeRecoverRollback.rawValue == LSQLITE_NOTICE_RECOVER_ROLLBACK) + #expect(ResultCode.warningAutoIndex.rawValue == LSQLITE_WARNING_AUTOINDEX) + #expect(ResultCode.authUser.rawValue == LSQLITE_AUTH_USER) + #expect(ResultCode.okLoadPermanently.rawValue == LSQLITE_OK_LOAD_PERMANENTLY) + } + + @Test("description and debugDescription cover known result codes") + func descriptionsCoverKnownResultCodes() { + for code in allResultCodes { + #expect(!code.description.isEmpty) + #expect(!code.debugDescription.isEmpty) + } + } + + @Test("unknown result codes use fallback strings") + func unknownResultCodesUseFallbackStrings() { + let unknown = ResultCode(rawValue: -12345) + #expect(unknown.description == "unknown") + #expect(unknown.debugDescription == "-12345") + } + + @Test("errorString returns a message when available") + func errorStringProvidesMessage() { + let message = ResultCode.ok.errorString + #expect(message != nil) + #expect(message != "") + } +} + +private let allResultCodes: [ResultCode] = [ + .ok, + .error, + .internal, + .permission, + .abort, + .busy, + .locked, + .noMemory, + .readonly, + .interrupt, + .ioError, + .corrupt, + .notFound, + .full, + .cantOpen, + .protocol, + .empty, + .schema, + .tooBig, + .constraint, + .mismatch, + .misuse, + .noLFS, + .auth, + .format, + .range, + .notADB, + .notice, + .warning, + .row, + .done, + .errorMissingCollSeq, + .errorRetry, + .errorSnapshot, + .ioErrorRead, + .ioErrorShortRead, + .ioErrorWrite, + .ioErrorFSync, + .ioErrorDirFSync, + .ioErrorTruncate, + .ioErrorFStat, + .ioErrorUnlock, + .ioErrorRDLock, + .ioErrorDelete, + .ioErrorBlocked, + .ioErrorNoMem, + .ioErrorAccess, + .ioErrorCheckReservedLock, + .ioErrorLock, + .ioErrorClose, + .ioErrorDirClose, + .ioErrorShMOpen, + .ioErrorShMSize, + .ioErrorShMLock, + .ioErrorShMMap, + .ioErrorSeek, + .ioErrorDeleteNoEnt, + .ioErrorMMap, + .ioErrorGetTempPath, + .ioErrorConvPath, + .ioErrorVNode, + .ioErrorAuth, + .ioErrorBeginAtomic, + .ioErrorCommitAtomic, + .ioErrorRollbackAtomic, + .lockedSharedCache, + .lockedVTab, + .busyRecovery, + .busySnapshot, + .cantOpenNoTempDir, + .cantOpenIsDir, + .cantOpenFullPath, + .cantOpenConvPath, + .cantOpenDirtyWAL, + .corruptVTab, + .corruptSequence, + .readonlyRecovery, + .readonlyCantLock, + .readonlyRollback, + .readonlyDBMoved, + .readonlyCantInit, + .readonlyDirectory, + .abortRollback, + .constraintCheck, + .constraintCommitHook, + .constraintForeignKey, + .constraintFunction, + .constraintNotNull, + .constraintPrimaryKey, + .constraintTrigger, + .constraintUnique, + .constraintVTab, + .constraintRowID, + .noticeRecoverWAL, + .noticeRecoverRollback, + .warningAutoIndex, + .authUser, + .okLoadPermanently, +] diff --git a/Tests/LSQLiteTests/RowIDTests.swift b/Tests/LSQLiteTests/RowIDTests.swift new file mode 100644 index 0000000..58bc3bc --- /dev/null +++ b/Tests/LSQLiteTests/RowIDTests.swift @@ -0,0 +1,19 @@ +import LSQLite +import Testing + +@Suite("RowID") +struct RowIDRawValueTests { + @Test("init(rawValue:) preserves rawValue") + func rawValueRoundTrip() { + let rawValue = Int64(9_876_543_210) + let rowID = RowID(rawValue: rawValue) + #expect(rowID.rawValue == rawValue) + } + + @Test("description reflects rawValue") + func descriptionReflectsRawValue() { + let rawValue = Int64(42) + let rowID = RowID(rawValue: rawValue) + #expect(rowID.description == "42") + } +} diff --git a/Tests/LSQLiteTests/Statement/Statement+BindTests.swift b/Tests/LSQLiteTests/Statement/Statement+BindTests.swift new file mode 100644 index 0000000..068021c --- /dev/null +++ b/Tests/LSQLiteTests/Statement/Statement+BindTests.swift @@ -0,0 +1,103 @@ +import LSQLite +import Testing + +@Suite("Statement+Bind") +final class StatementBindTests { + private let database: Database + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + self.database = openDatabase + try #require(openDatabase.exec(""" + CREATE TABLE bindings( + int_value INTEGER, + double_value REAL, + text_value TEXT, + blob_value BLOB, + zero_blob BLOB, + null_value TEXT, + int64_value INTEGER + ) + """) == .ok) + } + + deinit { + _ = database.close() + } + + @Test("binding metadata is available") + func bindingMetadataIsAvailable() throws { + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT ?1, :name, ?3", for: database) == .ok) + let prepared = try #require(statement) + #expect(prepared.bindingCount == 3) + let firstName = prepared.bindingName(at: 1) + #expect(firstName == nil || firstName == "?1") + let secondName = prepared.bindingName(at: 2) + #expect(secondName?.contains("name") == true) + let indexWithPrefix = prepared.bindingIndex(with: ":name") + let indexWithoutPrefix = prepared.bindingIndex(with: "name") + #expect(indexWithPrefix == 2 || indexWithoutPrefix == 2) + #expect(prepared.finalize() == .ok) + } + + @Test("binds scalar and blob values") + func bindsScalarAndBlobValues() throws { + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "INSERT INTO bindings(int_value, double_value, text_value, blob_value, zero_blob, null_value, int64_value) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", for: database) == .ok) + let prepared = try #require(statement) + + #expect(prepared.bindInt(12, at: 1) == .ok) + #expect(prepared.bindDouble(12.5, at: 2) == .ok) + #expect(prepared.bindText("hello", at: 3) == .ok) + + let blobBytes: [UInt8] = [0x01, 0x02, 0x03] + let transientResult = blobBytes.withUnsafeBytes { buffer -> ResultCode in + let baseAddress = buffer.baseAddress! + return prepared.bindTransientBlob(baseAddress, length: Int32(buffer.count), at: 4) + } + #expect(transientResult == .ok) + + #expect(prepared.bindZeroBlob(length: 4, at: 5) == .ok) + #expect(prepared.bindNull(at: 6) == .ok) + #expect(prepared.bindInt64(9_000_000_000, at: 7) == .ok) + #expect(prepared.clearBindings() == .ok) + + #expect(prepared.bindInt(1, at: 1) == .ok) + #expect(prepared.bindDouble(2.5, at: 2) == .ok) + #expect(prepared.bindText("again", at: 3) == .ok) + let staticBytes: [UInt8] = [0xAA, 0xBB] + let staticResult = staticBytes.withUnsafeBytes { buffer -> ResultCode in + let baseAddress = buffer.baseAddress! + return prepared.bindStaticBlob(baseAddress, length: Int32(buffer.count), at: 4) + } + #expect(staticResult == .ok) + #expect(prepared.bindZeroBlob(length: 2, at: 5) == .ok) + #expect(prepared.bindNull(at: 6) == .ok) + #expect(prepared.bindInt64(2, at: 7) == .ok) + + #expect(prepared.step() == .done) + #expect(prepared.reset() == .ok) + #expect(prepared.finalize() == .ok) + } + + @Test("bindBlob uses destructor") + func bindBlobUsesDestructor() throws { + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "INSERT INTO bindings(blob_value) VALUES (?1)", for: database) == .ok) + let prepared = try #require(statement) + + let pointer = UnsafeMutableRawPointer.allocate(byteCount: 2, alignment: MemoryLayout.alignment) + pointer.storeBytes(of: UInt8(0xCC), as: UInt8.self) + pointer.advanced(by: 1).storeBytes(of: UInt8(0xDD), as: UInt8.self) + #expect(prepared.bindBlob(pointer, length: 2, at: 1, destructor: blobDestructor) == .ok) + #expect(prepared.step() == .done) + #expect(prepared.finalize() == .ok) + } +} + +private func blobDestructor(_ blob: UnsafeMutableRawPointer?) { + blob?.deallocate() +} diff --git a/Tests/LSQLiteTests/Statement/Statement+BusyTests.swift b/Tests/LSQLiteTests/Statement/Statement+BusyTests.swift new file mode 100644 index 0000000..3b1f12a --- /dev/null +++ b/Tests/LSQLiteTests/Statement/Statement+BusyTests.swift @@ -0,0 +1,32 @@ +import LSQLite +import Testing + +@Suite("Statement+Busy") +final class StatementBusyTests { + private let database: Database + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + self.database = try #require(database) + } + + deinit { + _ = database.close() + } + + @Test("isBusy reflects step progress") + func isBusyReflectsStepProgress() throws { + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT 1 UNION ALL SELECT 2", for: database) == .ok) + let prepared = try #require(statement) + + #expect(!prepared.isBusy) + #expect(prepared.step() == .row) + #expect(prepared.isBusy) + #expect(prepared.step() == .row) + #expect(prepared.step() == .done) + #expect(!prepared.isBusy) + #expect(prepared.finalize() == .ok) + } +} diff --git a/Tests/LSQLiteTests/Statement/Statement+ColumnTests.swift b/Tests/LSQLiteTests/Statement/Statement+ColumnTests.swift new file mode 100644 index 0000000..252afee --- /dev/null +++ b/Tests/LSQLiteTests/Statement/Statement+ColumnTests.swift @@ -0,0 +1,88 @@ +import LSQLite +import Testing + +@Suite("Statement+Column") +final class StatementColumnTests { + private let database: Database + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + self.database = openDatabase + try #require(openDatabase.exec(""" + CREATE TABLE col_table( + text_col TEXT, + blob_col BLOB, + int_col INTEGER, + real_col REAL, + declared_col TEXT + ) + """) == .ok) + try #require(openDatabase.exec("INSERT INTO col_table(text_col, blob_col, int_col, real_col, declared_col) VALUES ('', X'0102', 42, 3.5, 'x')") == .ok) + } + + deinit { + _ = database.close() + } + + @Test("column metadata and values are available") + func columnMetadataAndValuesAreAvailable() throws { + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT text_col, blob_col, int_col, real_col, declared_col, NULL AS null_col, 99 AS literal_col FROM col_table", for: database) == .ok) + let prepared = try #require(statement) + + #expect(prepared.columnCount == 7) + #expect(prepared.dataCount == 0) + #expect(prepared.step() == .row) + #expect(prepared.dataCount == 7) + + #expect(prepared.columnName(at: 0) == "text_col") + #expect(prepared.columnName(at: 5) == "null_col") + #expect(prepared.columnName(at: 9) == nil) + + #expect(prepared.columnDatabaseName(at: 0) == "main") + #expect(prepared.columnTableName(at: 0) == "col_table") + #expect(prepared.columnOriginName(at: 0) == "text_col") + #expect(prepared.columnDatabaseName(at: 6) == nil) + #expect(prepared.columnTableName(at: 6) == nil) + #expect(prepared.columnOriginName(at: 6) == nil) + + #expect(prepared.columnDeclaredType(at: 4) == "TEXT") + #expect(prepared.columnDeclaredType(at: 6) == nil) + + #expect(prepared.columnBlob(at: 1) != nil) + #expect(prepared.columnDouble(at: 3) == 3.5) + #expect(prepared.columnInt(at: 2) == 42) + #expect(prepared.columnInt64(at: 2) == 42) + #expect(prepared.columnText(at: 0) == nil) + #expect(prepared.columnText(at: 4) == "x") + #expect(prepared.columnText(at: 5) == nil) + #expect(prepared.columnBytes(at: 1) == 2) + #expect(prepared.columnType(at: 0) == .text) + #expect(prepared.columnType(at: 5) == .null) + + #expect(prepared.step() == .done) + #expect(prepared.finalize() == .ok) + } + + @Test("column metadata treats empty identifiers as nil") + func columnMetadataTreatsEmptyIdentifiersAsNil() throws { + try #require(database.exec("ATTACH DATABASE ':memory:' AS \"\"") == .ok) + try #require(database.exec("CREATE TABLE \"\".\"\" (\"\" \"\")") == .ok) + try #require(database.exec("INSERT INTO \"\".\"\" VALUES ('x')") == .ok) + + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT \"\" FROM \"\".\"\"", for: database) == .ok) + let prepared = try #require(statement) + + #expect(prepared.step() == .row) + #expect(prepared.columnName(at: 0) == nil) + #expect(prepared.columnDatabaseName(at: 0) == nil) + #expect(prepared.columnTableName(at: 0) == nil) + #expect(prepared.columnOriginName(at: 0) == nil) + #expect(prepared.columnDeclaredType(at: 0) == nil) + #expect(prepared.step() == .done) + #expect(prepared.finalize() == .ok) + } +} diff --git a/Tests/LSQLiteTests/Statement/Statement+DatabaseTests.swift b/Tests/LSQLiteTests/Statement/Statement+DatabaseTests.swift new file mode 100644 index 0000000..a8bff53 --- /dev/null +++ b/Tests/LSQLiteTests/Statement/Statement+DatabaseTests.swift @@ -0,0 +1,19 @@ +import LSQLite +import Testing + +@Suite("Statement+Database") +struct StatementDatabaseTests { + @Test("database returns owning connection") + func databaseReturnsOwningConnection() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT 1", for: openDatabase) == .ok) + let prepared = try #require(statement) + #expect(prepared.database?.rawValue == openDatabase.rawValue) + #expect(prepared.finalize() == .ok) + _ = openDatabase.close() + } +} diff --git a/Tests/LSQLiteTests/Statement/Statement+FinalizeTests.swift b/Tests/LSQLiteTests/Statement/Statement+FinalizeTests.swift new file mode 100644 index 0000000..ef62408 --- /dev/null +++ b/Tests/LSQLiteTests/Statement/Statement+FinalizeTests.swift @@ -0,0 +1,18 @@ +import LSQLite +import Testing + +@Suite("Statement+Finalize") +struct StatementFinalizeTests { + @Test("finalize releases statement") + func finalizeReleasesStatement() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT 1", for: openDatabase) == .ok) + let prepared = try #require(statement) + #expect(prepared.finalize() == .ok) + _ = openDatabase.close() + } +} diff --git a/Tests/LSQLiteTests/Statement/Statement+PrepareTests.swift b/Tests/LSQLiteTests/Statement/Statement+PrepareTests.swift new file mode 100644 index 0000000..8132e83 --- /dev/null +++ b/Tests/LSQLiteTests/Statement/Statement+PrepareTests.swift @@ -0,0 +1,77 @@ +import LSQLite +import MissedSwiftSQLite +import Testing + +@Suite("Statement+Prepare") +final class StatementPrepareFlagRawValueTests { + private let database: Database + + init() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + self.database = try #require(database) + } + + deinit { + _ = database.close() + } + @Test("init(rawValue:) preserves rawValue") + func rawValueRoundTrip() { + let rawValue = UInt32(0x10) + let prepareFlag = Statement.PrepareFlag(rawValue: rawValue) + #expect(prepareFlag.rawValue == rawValue) + } + + @Test("PrepareFlag init(rawValue:) preserves combined rawValue") + func combinedRawValueRoundTrip() { + let rawValue = Statement.PrepareFlag.persistent.rawValue | Statement.PrepareFlag.normalize.rawValue + let prepareFlag = Statement.PrepareFlag(rawValue: rawValue) + #expect(prepareFlag.rawValue == rawValue) + } + + @Test("PrepareFlag constants match SQLite") + func prepareFlagConstantsMatchSQLite() { + #expect(Statement.PrepareFlag.persistent.rawValue == UInt32(SQLITE_PREPARE_PERSISTENT)) + #expect(Statement.PrepareFlag.normalize.rawValue == UInt32(SQLITE_PREPARE_NORMALIZE)) + #expect(Statement.PrepareFlag.noVTab.rawValue == UInt32(SQLITE_PREPARE_NO_VTAB)) + } + + @Test("PrepareFlag descriptions map values") + func prepareFlagDescriptions() { + #expect(Statement.PrepareFlag([]).description == "[]") + #expect(Statement.PrepareFlag.persistent.description.contains(".persistent")) + #expect(Statement.PrepareFlag(rawValue: 0x80).description == "unknown") + let mixed = Statement.PrepareFlag(rawValue: Statement.PrepareFlag.normalize.rawValue | 0x80) + #expect(mixed.description.contains("unknown")) + #expect(Statement.PrepareFlag.persistent.debugDescription.contains("SQLITE_PREPARE_PERSISTENT")) + #expect(mixed.debugDescription.contains("0x")) + #expect(Statement.PrepareFlag(rawValue: 0x80).debugDescription.hasPrefix("0x")) + } + + @Test("prepare compiles SQL and provides tail") + func prepareCompilesSQLAndProvidesTail() throws { + var statement: Statement? + var tail: String? + try #require(Statement.prepare(&statement, sql: "SELECT 1; SELECT 2", tail: &tail, for: database) == .ok) + let prepared = try #require(statement) + #expect(tail?.contains("SELECT 2") == true) + #expect(prepared.finalize() == .ok) + } + + @Test("prepare compiles SQL without tail") + func prepareCompilesSQLWithoutTail() throws { + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT 1", for: database) == .ok) + let prepared = try #require(statement) + #expect(prepared.finalize() == .ok) + } + + @Test("prepare with flags compiles SQL") + @available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 5.0, *) + func prepareWithFlagsCompilesSQL() throws { + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT 1", for: database, prepareFlag: [.persistent]) == .ok) + let prepared = try #require(statement) + #expect(prepared.finalize() == .ok) + } +} diff --git a/Tests/LSQLiteTests/Statement/Statement+ReadonlyTests.swift b/Tests/LSQLiteTests/Statement/Statement+ReadonlyTests.swift new file mode 100644 index 0000000..21f8fbc --- /dev/null +++ b/Tests/LSQLiteTests/Statement/Statement+ReadonlyTests.swift @@ -0,0 +1,27 @@ +import LSQLite +import Testing + +@Suite("Statement+Readonly") +struct StatementReadonlyTests { + @Test("isReadonly reflects statement type") + func isReadonlyReflectsStatementType() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + try #require(openDatabase.exec("CREATE TABLE items(id INTEGER)") == .ok) + + var selectStatement: Statement? + try #require(Statement.prepare(&selectStatement, sql: "SELECT id FROM items", for: openDatabase) == .ok) + let selectPrepared = try #require(selectStatement) + #expect(selectPrepared.isReadonly) + #expect(selectPrepared.finalize() == .ok) + + var insertStatement: Statement? + try #require(Statement.prepare(&insertStatement, sql: "INSERT INTO items(id) VALUES (1)", for: openDatabase) == .ok) + let insertPrepared = try #require(insertStatement) + #expect(!insertPrepared.isReadonly) + #expect(insertPrepared.finalize() == .ok) + + _ = openDatabase.close() + } +} diff --git a/Tests/LSQLiteTests/Statement/Statement+ResetTests.swift b/Tests/LSQLiteTests/Statement/Statement+ResetTests.swift new file mode 100644 index 0000000..6742050 --- /dev/null +++ b/Tests/LSQLiteTests/Statement/Statement+ResetTests.swift @@ -0,0 +1,22 @@ +import LSQLite +import Testing + +@Suite("Statement+Reset") +struct StatementResetTests { + @Test("reset allows re-stepping the statement") + func resetAllowsReStepping() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT 1", for: openDatabase) == .ok) + let prepared = try #require(statement) + #expect(prepared.step() == .row) + #expect(prepared.step() == .done) + #expect(prepared.reset() == .ok) + #expect(prepared.step() == .row) + #expect(prepared.finalize() == .ok) + _ = openDatabase.close() + } +} diff --git a/Tests/LSQLiteTests/Statement/Statement+SQLTests.swift b/Tests/LSQLiteTests/Statement/Statement+SQLTests.swift new file mode 100644 index 0000000..a2360e8 --- /dev/null +++ b/Tests/LSQLiteTests/Statement/Statement+SQLTests.swift @@ -0,0 +1,36 @@ +import LSQLite +import Testing + +@Suite("Statement+SQL") +struct StatementSQLTests { + @Test("sql returns original statement text") + func sqlReturnsOriginalStatementText() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT ?1", for: openDatabase) == .ok) + let prepared = try #require(statement) + #expect(prepared.sql == "SELECT ?1") + #expect(prepared.finalize() == .ok) + _ = openDatabase.close() + } + + @Test("expandedSql includes bound values") + @available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *) + func expandedSqlIncludesBoundValues() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT ?1", for: openDatabase) == .ok) + let prepared = try #require(statement) + #expect(prepared.bindInt(5, at: 1) == .ok) + let expanded = prepared.expandedSql + #expect(expanded?.contains("5") == true) + #expect(prepared.finalize() == .ok) + _ = openDatabase.close() + } +} diff --git a/Tests/LSQLiteTests/Statement/Statement+StepTests.swift b/Tests/LSQLiteTests/Statement/Statement+StepTests.swift new file mode 100644 index 0000000..f309385 --- /dev/null +++ b/Tests/LSQLiteTests/Statement/Statement+StepTests.swift @@ -0,0 +1,21 @@ +import LSQLite +import Testing + +@Suite("Statement+Step") +struct StatementStepTests { + @Test("step advances through rows") + func stepAdvancesThroughRows() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT 1 UNION ALL SELECT 2", for: openDatabase) == .ok) + let prepared = try #require(statement) + #expect(prepared.step() == .row) + #expect(prepared.step() == .row) + #expect(prepared.step() == .done) + #expect(prepared.finalize() == .ok) + _ = openDatabase.close() + } +} diff --git a/Tests/LSQLiteTests/Statement/StatementTests.swift b/Tests/LSQLiteTests/Statement/StatementTests.swift new file mode 100644 index 0000000..cf5c2c3 --- /dev/null +++ b/Tests/LSQLiteTests/Statement/StatementTests.swift @@ -0,0 +1,12 @@ +import LSQLite +import Testing + +@Suite("Statement") +struct StatementRawValueTests { + @Test("init(rawValue:) preserves rawValue") + func rawValueRoundTrip() { + let rawValue = OpaquePointer(bitPattern: 0x2)! + let statement = Statement(rawValue: rawValue) + #expect(statement.rawValue == rawValue) + } +} diff --git a/Tests/LSQLiteTests/StatementTests.swift b/Tests/LSQLiteTests/StatementTests.swift deleted file mode 100644 index a6c1544..0000000 --- a/Tests/LSQLiteTests/StatementTests.swift +++ /dev/null @@ -1,87 +0,0 @@ -import XCTest -import LSQLite - -class StatementTests: XCTestCase { - - var database: Database! - - override func setUpWithError() throws { - try super.setUpWithError() - database = try { - var database: Database? = nil - guard Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create, .memory]) == .ok, database != nil else { - if let database = database { - throw Error.result(database.lastErrorCode) - } else { - throw Error.unknown - } - } - return database - }() - } - - override func tearDownWithError() throws { - try super.tearDownWithError() - guard database.close() == .ok else { - throw Error.result(database.lastErrorCode) - } - } - - func testCreateDatabaseInsertRowRowsAndGetThemBack() throws { - let createTableStatement: Statement = try { - var statement: Statement? = nil - let resultCode = Statement.prepare(&statement, sql: "CREATE TABLE t(num INTEGER, txt TEXT);", for: database) - guard resultCode == .ok, statement != nil else { - throw Error.result(resultCode) - } - return statement! - }() - XCTAssertEqual(createTableStatement.step(), .done) - XCTAssertEqual(createTableStatement.finalize(), .ok) - - let insertStatement: Statement = try { - var statement: Statement? = nil - let resultCode = Statement.prepare(&statement, sql: "INSERT INTO t VALUES(?, ?);", for: database) - guard resultCode == .ok, statement != nil else { - throw Error.result(resultCode) - } - return statement! - }() - - XCTAssertEqual(insertStatement.bindInt(1, at: 1), .ok) - XCTAssertEqual(insertStatement.bindText("text1", at: 2), .ok) - XCTAssertEqual(insertStatement.step(), .done) - XCTAssertEqual(insertStatement.clearBindings(), .ok) - XCTAssertEqual(insertStatement.reset(), .ok) - - XCTAssertEqual(insertStatement.bindInt(2, at: 1), .ok) - XCTAssertEqual(insertStatement.bindNull(at: 2), .ok) - XCTAssertEqual(insertStatement.step(), .done) - XCTAssertEqual(insertStatement.clearBindings(), .ok) - XCTAssertEqual(insertStatement.reset(), .ok) - - XCTAssertEqual(insertStatement.finalize(), .ok) - - let selectStatement: Statement = try { - var statement: Statement? = nil - let resultCode = Statement.prepare(&statement, sql: "SELECT num, txt FROM t;", for: database) - guard resultCode == .ok, statement != nil else { - throw Error.result(resultCode) - } - return statement! - }() - - XCTAssertEqual(selectStatement.step(), .row) - XCTAssertEqual(selectStatement.columnInt(at: 0), 1) - XCTAssertEqual(selectStatement.columnText(at: 1), "text1") - - XCTAssertEqual(selectStatement.step(), .row) - XCTAssertEqual(selectStatement.columnInt(at: 0), 2) - XCTAssertEqual(selectStatement.columnText(at: 1), nil) - - XCTAssertEqual(selectStatement.step(), .done) - - XCTAssertEqual(selectStatement.finalize(), .ok) - } - -} diff --git a/Tests/LSQLiteTests/SubtypeTests.swift b/Tests/LSQLiteTests/SubtypeTests.swift new file mode 100644 index 0000000..00e4947 --- /dev/null +++ b/Tests/LSQLiteTests/SubtypeTests.swift @@ -0,0 +1,18 @@ +import LSQLite +import Testing + +@Suite("Subtype") +struct SubtypeRawValueTests { + @Test("init(rawValue:) preserves rawValue") + func rawValueRoundTrip() { + let rawValue = UInt32(255) + let subtype = Subtype(rawValue: rawValue) + #expect(subtype.rawValue == rawValue) + } + + @Test("description reflects rawValue") + func descriptionReflectsRawValue() { + let subtype = Subtype(rawValue: 7) + #expect(subtype.description == "7") + } +} diff --git a/Tests/LSQLiteTests/Value/Value+GettersTests.swift b/Tests/LSQLiteTests/Value/Value+GettersTests.swift new file mode 100644 index 0000000..cad95ca --- /dev/null +++ b/Tests/LSQLiteTests/Value/Value+GettersTests.swift @@ -0,0 +1,97 @@ +import LSQLite +import Testing + +@Suite("Value+Getters") +struct ValueGettersTests { + @Test("value getters expose underlying data") + func valueGettersExposeUnderlyingData() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + + let probe = ValueGetterProbe() + let userData = Unmanaged.passUnretained(probe).toOpaque() + #expect(openDatabase.createFunction(name: "value_getters", argumentCount: 5, textEncoding: .utf8, userData: userData, funcHandler: valueGetterHandler) == .ok) + + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT value_getters(?1, ?2, ?3, ?4, ?5)", for: openDatabase) == .ok) + let prepared = try #require(statement) + + let blobBytes: [UInt8] = [0x01, 0x02, 0x03] + let blobResult = blobBytes.withUnsafeBytes { buffer -> ResultCode in + prepared.bindTransientBlob(buffer.baseAddress!, length: Int32(buffer.count), at: 1) + } + #expect(blobResult == .ok) + #expect(prepared.bindDouble(1.5, at: 2) == .ok) + #expect(prepared.bindInt(7, at: 3) == .ok) + #expect(prepared.bindInt64(9_000_000_000, at: 4) == .ok) + #expect(prepared.bindText("text", at: 5) == .ok) + + #expect(prepared.step() == .row) + #expect(prepared.step() == .done) + #expect(prepared.finalize() == .ok) + + #expect(!probe.blobIsNil) + #expect(probe.doubleValue == 1.5) + #expect(probe.intValue == 7) + #expect(probe.int64Value == 9_000_000_000) + #expect(probe.textValue == "text") + _ = openDatabase.close() + } + + @Test("text getter returns nil for NULL values") + func textGetterReturnsNilForNullValues() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + + let probe = ValueGetterProbe() + let userData = Unmanaged.passUnretained(probe).toOpaque() + #expect(openDatabase.createFunction(name: "value_getters", argumentCount: 5, textEncoding: .utf8, userData: userData, funcHandler: valueGetterHandler) == .ok) + + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT value_getters(?1, ?2, ?3, ?4, ?5)", for: openDatabase) == .ok) + let prepared = try #require(statement) + + #expect(prepared.bindInt(1, at: 1) == .ok) + #expect(prepared.bindInt(2, at: 2) == .ok) + #expect(prepared.bindInt(3, at: 3) == .ok) + #expect(prepared.bindInt(4, at: 4) == .ok) + #expect(prepared.bindNull(at: 5) == .ok) + + #expect(prepared.step() == .row) + #expect(prepared.step() == .done) + #expect(prepared.finalize() == .ok) + + #expect(probe.textValue == nil) + _ = openDatabase.close() + } +} + +private final class ValueGetterProbe { + var blobIsNil = true + var doubleValue: Double = 0 + var intValue: Int32 = 0 + var int64Value: Int64 = 0 + var textValue: String? +} + +private func valueGetterHandler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context, let values else { return } + let wrapper = Context(rawValue: context) + if let userData = wrapper.userData { + let probe = Unmanaged.fromOpaque(userData).takeUnretainedValue() + let blobValue = Value(rawValue: values[0]!) + let doubleValue = Value(rawValue: values[1]!) + let intValue = Value(rawValue: values[2]!) + let int64Value = Value(rawValue: values[3]!) + let textValue = Value(rawValue: values[4]!) + + probe.blobIsNil = (blobValue.blob == nil) + probe.doubleValue = doubleValue.double + probe.intValue = intValue.int + probe.int64Value = int64Value.int64 + probe.textValue = textValue.text + } + wrapper.resultInt(1) +} diff --git a/Tests/LSQLiteTests/Value/Value+IntrospectTests.swift b/Tests/LSQLiteTests/Value/Value+IntrospectTests.swift new file mode 100644 index 0000000..7adde90 --- /dev/null +++ b/Tests/LSQLiteTests/Value/Value+IntrospectTests.swift @@ -0,0 +1,62 @@ +import LSQLite +import Testing + +@Suite("Value+Introspect") +struct ValueIntrospectTests { + @Test("value introspection reports metadata") + func valueIntrospectionReportsMetadata() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + + var probe = ValueIntrospectProbe() + #expect(openDatabase.createFunction(name: "value_introspect", argumentCount: 1, textEncoding: .utf8, userData: &probe, funcHandler: valueIntrospectHandler) == .ok) + + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT value_introspect(?1)", for: openDatabase) == .ok) + let prepared = try #require(statement) + #expect(prepared.bindText("123", at: 1) == .ok) + #expect(prepared.step() == .row) + #expect(prepared.step() == .done) + #expect(prepared.finalize() == .ok) + + #expect(probe.byteCount == 3) + #expect(probe.typeRawValue == Datatype.text.rawValue) + #expect(probe.numericTypeRawValue == Datatype.integer.rawValue) + if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) { + #expect(probe.isFromBind) + } + _ = openDatabase.close() + } +} + +private struct ValueIntrospectProbe { + var byteCount: Int32 = 0 + var typeRawValue: Int32 = 0 + var numericTypeRawValue: Int32 = 0 + var noChange: Bool = false + var isFromBind: Bool = false + var subtypeRawValue: UInt32 = 0 +} + +private func valueIntrospectHandler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context, let values else { return } + let wrapper = Context(rawValue: context) + if let userData = wrapper.userData { + let probe = userData.assumingMemoryBound(to: ValueIntrospectProbe.self) + let value = Value(rawValue: values[0]!) + probe.pointee.byteCount = value.byteCount() + probe.pointee.typeRawValue = value.type.rawValue + probe.pointee.numericTypeRawValue = value.convertToNumericType().rawValue + if #available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 5.0, *) { + probe.pointee.noChange = value.noChange + } + if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) { + probe.pointee.isFromBind = value.isFromBind + } + if #available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *) { + probe.pointee.subtypeRawValue = value.subtype.rawValue + } + } + wrapper.resultInt(1) +} diff --git a/Tests/LSQLiteTests/Value/Value+MemoryTests.swift b/Tests/LSQLiteTests/Value/Value+MemoryTests.swift new file mode 100644 index 0000000..86ae7c4 --- /dev/null +++ b/Tests/LSQLiteTests/Value/Value+MemoryTests.swift @@ -0,0 +1,47 @@ +import LSQLite +import Testing + +@Suite("Value+Memory") +struct ValueMemoryTests { + @Test("createCopy and free round-trip values") + @available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *) + func createCopyAndFreeRoundTripValues() throws { + var database: Database? + try #require(Database.open(&database, at: .memory, withOpenFlags: [.readwrite, .create]) == .ok) + let openDatabase = try #require(database) + + var probe = ValueMemoryProbe() + #expect(openDatabase.createFunction(name: "value_copy", argumentCount: 1, textEncoding: .utf8, userData: &probe, funcHandler: valueCopyHandler) == .ok) + + var statement: Statement? + try #require(Statement.prepare(&statement, sql: "SELECT value_copy(?1)", for: openDatabase) == .ok) + let prepared = try #require(statement) + #expect(prepared.bindInt(99, at: 1) == .ok) + #expect(prepared.step() == .row) + #expect(prepared.step() == .done) + #expect(prepared.finalize() == .ok) + + #expect(probe.copied) + _ = openDatabase.close() + } +} + +private struct ValueMemoryProbe { + var copied = false +} + +@available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *) +private func valueCopyHandler(_ context: OpaquePointer?, _ valueCount: Int32, _ values: UnsafeMutablePointer?) { + guard let context, let values else { return } + let wrapper = Context(rawValue: context) + if let userData = wrapper.userData { + let probe = userData.assumingMemoryBound(to: ValueMemoryProbe.self) + let value = Value(rawValue: values[0]!) + let copy = value.createCopy() + if let copy { + probe.pointee.copied = true + copy.free() + } + } + wrapper.resultInt(1) +} diff --git a/Tests/LSQLiteTests/Value/ValueTests.swift b/Tests/LSQLiteTests/Value/ValueTests.swift new file mode 100644 index 0000000..d78399e --- /dev/null +++ b/Tests/LSQLiteTests/Value/ValueTests.swift @@ -0,0 +1,12 @@ +import LSQLite +import Testing + +@Suite("Value") +struct ValueRawValueTests { + @Test("init(rawValue:) preserves rawValue") + func rawValueRoundTrip() { + let rawValue = OpaquePointer(bitPattern: 0x4)! + let value = Value(rawValue: rawValue) + #expect(value.rawValue == rawValue) + } +}