From bbb7f1465077d74f8a14ad3cb4f646b8c6a038fa Mon Sep 17 00:00:00 2001 From: Oleksii Zghurskyi Date: Sat, 25 Oct 2025 18:56:55 +0300 Subject: [PATCH] Add unit tests --- Package.swift | 6 +- .../Classes/Connection+Location.swift | 2 +- Sources/DataLiteCore/Classes/Connection.swift | 16 +- Sources/DataLiteCore/Classes/Statement.swift | 13 + Sources/DataLiteCore/Enums/JournalMode.swift | 24 +- .../DataLiteCore/Enums/TransactionType.swift | 30 +- .../Protocols/ConnectionDelegate.swift | 14 +- .../Protocols/ConnectionProtocol.swift | 6 +- .../Protocols/StatementProtocol.swift | 23 +- .../DataLiteCore/Structures/SQLiteError.swift | 2 +- .../Classes/Connection+KeyTests.swift | 6 +- .../Classes/Connection+LocationTests.swift | 6 +- .../Classes/Connection+OptionsTests.swift | 16 +- .../Classes/ConnectionTests.swift | 456 +++++++++++------- .../Classes/Function+OptionsTests.swift | 13 +- .../Classes/Function+RegexpTests.swift | 82 ++++ .../Classes/Statement+OptionsTests.swift | 12 +- .../Classes/StatementTests.swift | 197 ++++++-- .../Enums/JournalModeTests.swift | 33 ++ .../Enums/SQLiteActionTests.swift | 43 -- .../Enums/SQLiteValueTests.swift | 10 +- .../Enums/TransactionTypeTests.swift | 29 ++ .../Extensions/BinaryFloatingPointTests.swift | 54 +-- .../Extensions/BinaryIntegerTests.swift | 68 ++- .../Extensions/BoolTests.swift | 20 +- .../Extensions/DataTests.swift | 16 +- .../Extensions/DateTests.swift | 27 +- .../Extensions/RawRepresentableTests.swift | 27 +- .../Extensions/StringTests.swift | 7 +- .../Extensions/UUIDTests.swift | 13 +- .../Protocols/ArgumentsProtocolTests.swift | 46 ++ .../Protocols/SQLiteBindableTests.swift | 16 +- .../Resources/empty_script.sql | 1 - .../Resources/invalid_script.sql | Bin 793 -> 0 bytes .../Resources/valid_script.sql | 16 - .../Structures/PragmaTests.swift | 30 ++ .../Structures/SQLiteErrorTests.swift | 28 +- .../Structures/SQLiteRowTests.swift | 169 ++++--- 38 files changed, 1051 insertions(+), 526 deletions(-) create mode 100644 Tests/DataLiteCoreTests/Classes/Function+RegexpTests.swift create mode 100644 Tests/DataLiteCoreTests/Enums/JournalModeTests.swift delete mode 100644 Tests/DataLiteCoreTests/Enums/SQLiteActionTests.swift create mode 100644 Tests/DataLiteCoreTests/Enums/TransactionTypeTests.swift create mode 100644 Tests/DataLiteCoreTests/Protocols/ArgumentsProtocolTests.swift delete mode 100644 Tests/DataLiteCoreTests/Resources/empty_script.sql delete mode 100644 Tests/DataLiteCoreTests/Resources/invalid_script.sql delete mode 100644 Tests/DataLiteCoreTests/Resources/valid_script.sql create mode 100644 Tests/DataLiteCoreTests/Structures/PragmaTests.swift diff --git a/Package.swift b/Package.swift index e66894c..5ee8ce8 100644 --- a/Package.swift +++ b/Package.swift @@ -34,10 +34,8 @@ let package = Package( .testTarget( name: "DataLiteCoreTests", dependencies: ["DataLiteCore"], - resources: [ - .copy("Resources/valid_script.sql"), - .copy("Resources/empty_script.sql"), - .copy("Resources/invalid_script.sql") + cSettings: [ + .define("SQLITE_HAS_CODEC") ] ) ] diff --git a/Sources/DataLiteCore/Classes/Connection+Location.swift b/Sources/DataLiteCore/Classes/Connection+Location.swift index 26ed15a..d66da75 100644 --- a/Sources/DataLiteCore/Classes/Connection+Location.swift +++ b/Sources/DataLiteCore/Classes/Connection+Location.swift @@ -7,7 +7,7 @@ extension Connection { /// - ``file(path:)``: A database at a specific file path or URI (persistent). /// - ``inMemory``: An in-memory database that exists only in RAM. /// - ``temporary``: A temporary on-disk database deleted when the connection closes. - public enum Location { + public enum Location: Sendable { /// A database stored at a given file path or URI. /// /// Use this for persistent databases located on disk or referenced via SQLite URI. diff --git a/Sources/DataLiteCore/Classes/Connection.swift b/Sources/DataLiteCore/Classes/Connection.swift index ce5c129..d08389a 100644 --- a/Sources/DataLiteCore/Classes/Connection.swift +++ b/Sources/DataLiteCore/Classes/Connection.swift @@ -152,7 +152,7 @@ extension Connection: ConnectionProtocol { sqlite3_key(connection, key.keyValue, key.length) } guard status == SQLITE_OK else { - throw SQLiteError(connection) + throw SQLiteError(code: status, message: "") } } @@ -163,13 +163,15 @@ extension Connection: ConnectionProtocol { sqlite3_rekey(connection, key.keyValue, key.length) } guard status == SQLITE_OK else { - throw SQLiteError(connection) + throw SQLiteError(code: status, message: "") } } public func add(delegate: any ConnectionDelegate) { - delegates.append(.init(delegate: delegate)) - delegates.removeAll { $0.delegate == nil } + if !delegates.contains(where: { $0.delegate === delegate }) { + delegates.append(.init(delegate: delegate)) + delegates.removeAll { $0.delegate == nil } + } } public func remove(delegate: any ConnectionDelegate) { @@ -179,8 +181,10 @@ extension Connection: ConnectionProtocol { } public func add(trace delegate: any ConnectionTraceDelegate) { - traceDelegates.append(.init(delegate: delegate)) - traceDelegates.removeAll { $0.delegate == nil } + if !traceDelegates.contains(where: { $0.delegate === delegate }) { + traceDelegates.append(.init(delegate: delegate)) + traceDelegates.removeAll { $0.delegate == nil } + } } public func remove(trace delegate: any ConnectionTraceDelegate) { diff --git a/Sources/DataLiteCore/Classes/Statement.swift b/Sources/DataLiteCore/Classes/Statement.swift index 05aafcc..5aa7dfe 100644 --- a/Sources/DataLiteCore/Classes/Statement.swift +++ b/Sources/DataLiteCore/Classes/Statement.swift @@ -51,6 +51,19 @@ public final class Statement { // MARK: - StatementProtocol extension Statement: StatementProtocol { + public var sql: String? { + let cSQL = sqlite3_sql(statement) + guard let cSQL else { return nil } + return String(cString: cSQL) + } + + public var expandedSQL: String? { + let cSQL = sqlite3_expanded_sql(statement) + defer { sqlite3_free(cSQL) } + guard let cSQL else { return nil } + return String(cString: cSQL) + } + public func parameterCount() -> Int32 { sqlite3_bind_parameter_count(statement) } diff --git a/Sources/DataLiteCore/Enums/JournalMode.swift b/Sources/DataLiteCore/Enums/JournalMode.swift index 350166a..88c5e8a 100644 --- a/Sources/DataLiteCore/Enums/JournalMode.swift +++ b/Sources/DataLiteCore/Enums/JournalMode.swift @@ -73,12 +73,12 @@ public enum JournalMode: String, SQLiteRepresentable { /// - SeeAlso: [journal_mode](https://sqlite.org/pragma.html#pragma_journal_mode) public var rawValue: String { switch self { - case .delete: "DELETE" + case .delete: "DELETE" case .truncate: "TRUNCATE" - case .persist: "PERSIST" - case .memory: "MEMORY" - case .wal: "WAL" - case .off: "OFF" + case .persist: "PERSIST" + case .memory: "MEMORY" + case .wal: "WAL" + case .off: "OFF" } } @@ -95,13 +95,13 @@ public enum JournalMode: String, SQLiteRepresentable { /// - SeeAlso: [journal_mode](https://sqlite.org/pragma.html#pragma_journal_mode) public init?(rawValue: String) { switch rawValue.uppercased() { - case "DELETE": self = .delete - case "TRUNCATE": self = .truncate - case "PERSIST": self = .persist - case "MEMORY": self = .memory - case "WAL": self = .wal - case "OFF": self = .off - default: return nil + case "DELETE": self = .delete + case "TRUNCATE": self = .truncate + case "PERSIST": self = .persist + case "MEMORY": self = .memory + case "WAL": self = .wal + case "OFF": self = .off + default: return nil } } } diff --git a/Sources/DataLiteCore/Enums/TransactionType.swift b/Sources/DataLiteCore/Enums/TransactionType.swift index 1579d6d..02de957 100644 --- a/Sources/DataLiteCore/Enums/TransactionType.swift +++ b/Sources/DataLiteCore/Enums/TransactionType.swift @@ -13,24 +13,48 @@ public enum TransactionType: String, CustomStringConvertible { /// With `BEGIN DEFERRED`, no locks are acquired immediately. If the first statement is a read /// (`SELECT`), a read transaction begins. If it is a write statement, a write transaction /// begins instead. Deferred transactions allow greater concurrency and are the default mode. - case deferred = "DEFERRED" + case deferred /// Starts a write transaction immediately. /// /// With `BEGIN IMMEDIATE`, a reserved lock is acquired right away to ensure that no other /// connection can start a conflicting write. The statement may fail with `SQLITE_BUSY` if /// another write transaction is already active. - case immediate = "IMMEDIATE" + case immediate /// Starts an exclusive write transaction. /// /// With `BEGIN EXCLUSIVE`, a write lock is acquired immediately. In rollback journal mode, it /// also prevents other connections from reading the database while the transaction is active. /// In WAL mode, it behaves the same as `.immediate`. - case exclusive = "EXCLUSIVE" + case exclusive /// A textual representation of the transaction type. public var description: String { rawValue } + + /// Returns the SQLite keyword that represents the transaction type. + /// + /// The value is always uppercased to match the keywords used by SQLite statements. + public var rawValue: String { + switch self { + case .deferred: "DEFERRED" + case .immediate: "IMMEDIATE" + case .exclusive: "EXCLUSIVE" + } + } + + /// Creates a transaction type from an SQLite keyword. + /// + /// The initializer accepts any ASCII case variant of the keyword (`"deferred"`, `"Deferred"`, + /// etc.). Returns `nil` if the string does not correspond to a supported transaction type. + public init?(rawValue: String) { + switch rawValue.uppercased() { + case "DEFERRED": self = .deferred + case "IMMEDIATE": self = .immediate + case "EXCLUSIVE": self = .exclusive + default: return nil + } + } } diff --git a/Sources/DataLiteCore/Protocols/ConnectionDelegate.swift b/Sources/DataLiteCore/Protocols/ConnectionDelegate.swift index 9be6930..af04f04 100644 --- a/Sources/DataLiteCore/Protocols/ConnectionDelegate.swift +++ b/Sources/DataLiteCore/Protocols/ConnectionDelegate.swift @@ -2,11 +2,9 @@ import Foundation /// A delegate that observes connection-level database events. /// -/// Conforming types can monitor row-level updates and transaction lifecycle events. All methods are -/// optional — default implementations do nothing. -/// -/// This protocol is typically used for debugging, logging, or synchronizing application state with -/// database changes. +/// Conforming types can monitor row-level updates and transaction lifecycle events. This protocol +/// is typically used for debugging, logging, or synchronizing application state with database +/// changes. /// /// - Important: Delegate methods are invoked synchronously on SQLite’s internal execution thread. /// Implementations must be lightweight and non-blocking to avoid slowing down SQL operations. @@ -43,9 +41,3 @@ public protocol ConnectionDelegate: AnyObject { /// - Parameter connection: The connection that rolled back. func connectionDidRollback(_ connection: ConnectionProtocol) } - -public extension ConnectionDelegate { - func connection(_ connection: ConnectionProtocol, didUpdate action: SQLiteAction) {} - func connectionWillCommit(_ connection: ConnectionProtocol) throws {} - func connectionDidRollback(_ connection: ConnectionProtocol) {} -} diff --git a/Sources/DataLiteCore/Protocols/ConnectionProtocol.swift b/Sources/DataLiteCore/Protocols/ConnectionProtocol.swift index c83d3eb..1982b6c 100644 --- a/Sources/DataLiteCore/Protocols/ConnectionProtocol.swift +++ b/Sources/DataLiteCore/Protocols/ConnectionProtocol.swift @@ -12,10 +12,10 @@ import Foundation /// /// - ``isAutocommit`` /// - ``isReadonly`` -/// - ``busyTimeout`` /// /// ### Accessing PRAGMA Values /// +/// - ``busyTimeout`` /// - ``applicationID`` /// - ``foreignKeys`` /// - ``journalMode`` @@ -84,6 +84,8 @@ public protocol ConnectionProtocol: AnyObject { /// - SeeAlso: [Determine if a database is read-only](https://sqlite.org/c3ref/db_readonly.html) var isReadonly: Bool { get } + // MARK: - PRAGMA Accessors + /// The busy timeout of the connection, in milliseconds. /// /// Defines how long SQLite waits for a locked database to become available before returning @@ -93,8 +95,6 @@ public protocol ConnectionProtocol: AnyObject { /// - SeeAlso: [Set A Busy Timeout](https://sqlite.org/c3ref/busy_timeout.html) var busyTimeout: Int32 { get set } - // MARK: - PRAGMA Accessors - /// The application identifier stored in the database header. /// /// Used to distinguish database files created by different applications or file formats. This diff --git a/Sources/DataLiteCore/Protocols/StatementProtocol.swift b/Sources/DataLiteCore/Protocols/StatementProtocol.swift index a1321d8..15e9c17 100644 --- a/Sources/DataLiteCore/Protocols/StatementProtocol.swift +++ b/Sources/DataLiteCore/Protocols/StatementProtocol.swift @@ -8,6 +8,11 @@ import Foundation /// /// ## Topics /// +/// ### Retrieving Statement SQL +/// +/// - ``sql`` +/// - ``expandedSQL`` +/// /// ### Binding Parameters /// /// - ``parameterCount()`` @@ -34,6 +39,20 @@ import Foundation /// - ``columnValue(at:)->T?`` /// - ``currentRow()`` public protocol StatementProtocol { + // MARK: - Retrieving Statement SQL + + /// The original SQL text used to create this prepared statement. + /// + /// Returns the statement text as it was supplied when the statement was prepared. Useful for + /// diagnostics or query introspection. + var sql: String? { get } + + /// The SQL text with all parameter values expanded into their literal forms. + /// + /// Shows the actual SQL string that would be executed after parameter binding. Useful for + /// debugging and logging queries. + var expandedSQL: String? { get } + // MARK: - Binding Parameters /// Returns the number of parameters in the prepared SQLite statement. @@ -101,7 +120,7 @@ public protocol StatementProtocol { /// /// - Parameters: /// - value: The ``SQLiteValue`` to bind. - /// - name: The parameter name as written in SQL, including its prefix. + /// - name: The parameter name exactly as written in SQL, including its prefix. /// - Throws: ``SQLiteError`` if binding fails. /// /// - SeeAlso: [Binding Values To Prepared Statements]( @@ -130,7 +149,7 @@ public protocol StatementProtocol { /// /// - Parameters: /// - value: The value to bind. If `nil`, `NULL` is bound. - /// - name: The parameter name as written in SQL, including its prefix. + /// - name: The parameter name exactly as written in SQL, including its prefix. /// - Throws: ``SQLiteError`` if binding fails. /// /// - SeeAlso: [Binding Values To Prepared Statements]( diff --git a/Sources/DataLiteCore/Structures/SQLiteError.swift b/Sources/DataLiteCore/Structures/SQLiteError.swift index 60183fe..84c90a1 100644 --- a/Sources/DataLiteCore/Structures/SQLiteError.swift +++ b/Sources/DataLiteCore/Structures/SQLiteError.swift @@ -46,7 +46,7 @@ public struct SQLiteError: Error, Equatable, CustomStringConvertible, Sendable { /// name (`SQLiteError`), the numeric code, and the corresponding message, making it useful for /// debugging, logging, or diagnostic displays. public var description: String { - "\(Self.self) code: \(code) message: \(message)" + "\(Self.self)(\(code)): \(message)" } /// Creates a new error instance with the specified result code and message. diff --git a/Tests/DataLiteCoreTests/Classes/Connection+KeyTests.swift b/Tests/DataLiteCoreTests/Classes/Connection+KeyTests.swift index ba04ae4..c2b2641 100644 --- a/Tests/DataLiteCoreTests/Classes/Connection+KeyTests.swift +++ b/Tests/DataLiteCoreTests/Classes/Connection+KeyTests.swift @@ -3,20 +3,20 @@ import Foundation import DataLiteCore struct ConnectionKeyTests { - @Test func testPassphrase() { + @Test func passphrase() { let key = Connection.Key.passphrase("secret123") #expect(key.keyValue == "secret123") #expect(key.length == 9) } - @Test func testRawKey() { + @Test func rawKey() { let keyData = Data([0x01, 0xAB, 0xCD, 0xEF]) let key = Connection.Key.rawKey(keyData) #expect(key.keyValue == "X'01ABCDEF'") #expect(key.length == 11) } - @Test func testRawKeyLengthConsistency() { + @Test func rawKeyLengthConsistency() { let rawBytes = Data(repeating: 0x00, count: 32) let key = Connection.Key.rawKey(rawBytes) let hexPart = key.keyValue.dropFirst(2).dropLast() diff --git a/Tests/DataLiteCoreTests/Classes/Connection+LocationTests.swift b/Tests/DataLiteCoreTests/Classes/Connection+LocationTests.swift index 2865c04..0e97a82 100644 --- a/Tests/DataLiteCoreTests/Classes/Connection+LocationTests.swift +++ b/Tests/DataLiteCoreTests/Classes/Connection+LocationTests.swift @@ -2,18 +2,18 @@ import Testing @testable import DataLiteCore struct ConnectionLocationTests { - @Test func testFileLocationPath() { + @Test func fileLocationPath() { let filePath = "/path/to/database.db" let location = Connection.Location.file(path: filePath) #expect(location.path == filePath) } - @Test func testInMemoryLocationPath() { + @Test func inMemoryLocationPath() { let inMemoryLocation = Connection.Location.inMemory #expect(inMemoryLocation.path == ":memory:") } - @Test func testTemporaryLocationPath() { + @Test func temporaryLocationPath() { let temporaryLocation = Connection.Location.temporary #expect(temporaryLocation.path == "") } diff --git a/Tests/DataLiteCoreTests/Classes/Connection+OptionsTests.swift b/Tests/DataLiteCoreTests/Classes/Connection+OptionsTests.swift index 516d81c..c62cecc 100644 --- a/Tests/DataLiteCoreTests/Classes/Connection+OptionsTests.swift +++ b/Tests/DataLiteCoreTests/Classes/Connection+OptionsTests.swift @@ -3,40 +3,40 @@ import DataLiteC import DataLiteCore struct ConnectionOptionsTests { - @Test func testReadOnlyOption() { + @Test func readOnlyOption() { let options: Connection.Options = [.readonly] #expect(options.contains(.readonly)) } - @Test func testReadWriteOption() { + @Test func readWriteOption() { let options: Connection.Options = [.readwrite] #expect(options.contains(.readwrite)) } - @Test func testCreateOption() { + @Test func createOption() { let options: Connection.Options = [.create] #expect(options.contains(.create)) } - @Test func testMultipleOptions() { + @Test func multipleOptions() { let options: Connection.Options = [.readwrite, .create, .memory] #expect(options.contains(.readwrite)) #expect(options.contains(.create)) #expect(options.contains(.memory)) } - @Test func testNoFollowOption() { + @Test func noFollowOption() { let options: Connection.Options = [.nofollow] #expect(options.contains(.nofollow)) } - @Test func testAllOptions() { + @Test func allOptions() { let options: Connection.Options = [ .readonly, .readwrite, .create, .uri, .memory, .nomutex, .fullmutex, .sharedcache, .privatecache, .exrescode, .nofollow ] - + #expect(options.contains(.readonly)) #expect(options.contains(.readwrite)) #expect(options.contains(.create)) @@ -50,7 +50,7 @@ struct ConnectionOptionsTests { #expect(options.contains(.nofollow)) } - @Test func testOptionsRawValue() { + @Test func optionsRawValue() { let options: Connection.Options = [.readwrite, .create] let expectedRawValue = Int32(SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE) #expect(options.rawValue == expectedRawValue) diff --git a/Tests/DataLiteCoreTests/Classes/ConnectionTests.swift b/Tests/DataLiteCoreTests/Classes/ConnectionTests.swift index 222d21a..bd78b8f 100644 --- a/Tests/DataLiteCoreTests/Classes/ConnectionTests.swift +++ b/Tests/DataLiteCoreTests/Classes/ConnectionTests.swift @@ -1,73 +1,71 @@ -import Foundation import Testing +import Foundation import DataLiteC -import DataLiteCore + +@testable import DataLiteCore struct ConnectionTests { - @Test func testIsAutocommitInitially() throws { - let connection = try Connection( - location: .inMemory, - options: [.create, .readwrite] - ) - #expect(connection.isAutocommit == true) + @Test(arguments: [ + Connection.Location.inMemory, + Connection.Location.temporary + ]) + func initLocation(_ location: Connection.Location) throws { + let _ = try Connection(location: location, options: [.create, .readwrite]) } - @Test func testIsAutocommitDuringTransaction() throws { - let connection = try Connection( - location: .inMemory, - options: [.create, .readwrite] - ) - try connection.beginTransaction() - #expect(connection.isAutocommit == false) + @Test func initPath() throws { + let dir = FileManager.default.temporaryDirectory + let file = UUID().uuidString + let path = dir.appending(component: file).path + defer { try? FileManager.default.removeItem(atPath: path) } + let _ = try Connection(path: path, options: [.create, .readwrite]) } - @Test func testIsAutocommitAfterCommit() throws { - let connection = try Connection( - location: .inMemory, - options: [.create, .readwrite] + @Test func initPathFail() { + #expect( + throws: SQLiteError( + code: SQLITE_CANTOPEN, + message: "unable to open database file" + ), + performing: { + try Connection( + path: "/invalid-path/", + options: [.create, .readwrite] + ) + } ) - try connection.beginTransaction() - try connection.commitTransaction() - #expect(connection.isAutocommit == true) } - @Test func testIsAutocommitAfterRollback() throws { + @Test func isAutocommit() throws { let connection = try Connection( location: .inMemory, options: [.create, .readwrite] ) - try connection.beginTransaction() - try connection.rollbackTransaction() - #expect(connection.isAutocommit == true) + #expect(connection.isAutocommit) } @Test(arguments: [ (Connection.Options.readwrite, false), (Connection.Options.readonly, true) ]) - func testIsReadonly( - _ opt: Connection.Options, - _ isReadonly: Bool + func isReadonly( + _ options: Connection.Options, + _ expected: Bool ) throws { - let url = FileManager.default.temporaryDirectory - .appendingPathComponent(UUID().uuidString) - .appendingPathExtension("sqlite") - defer { try? FileManager.default.removeItem(at: url) } - let _ = try Connection( - location: .file(path: url.path), - options: [.create, .readwrite] - ) - let connection = try Connection( - location: .file(path: url.path), - options: [opt] - ) - #expect(connection.isReadonly == isReadonly) + let dir = FileManager.default.temporaryDirectory + let file = UUID().uuidString + let path = dir.appending(component: file).path + defer { try? FileManager.default.removeItem(atPath: path) } + + let _ = try Connection(path: path, options: [.create, .readwrite]) + let connection = try Connection(path: path, options: options) + + #expect(connection.isReadonly == expected) } @Test func testBusyTimeout() throws { let connection = try Connection( - location: .inMemory, - options: [.create, .readwrite] + location: .inMemory, options: [.create, .readwrite] ) connection.busyTimeout = 5000 #expect(try connection.get(pragma: .busyTimeout) == 5000) @@ -76,50 +74,9 @@ struct ConnectionTests { #expect(connection.busyTimeout == 1000) } - @Test func testBusyTimeoutSQLiteBusy() throws { - let url = FileManager.default.temporaryDirectory - .appendingPathComponent(UUID().uuidString) - .appendingPathExtension("sqlite") - defer { try? FileManager.default.removeItem(at: url) } - - let oneConn = try Connection( - location: .file(path: url.path), - options: [.create, .readwrite, .fullmutex] - ) - let twoConn = try Connection( - location: .file(path: url.path), - options: [.create, .readwrite, .fullmutex] - ) - - try oneConn.execute(sql: """ - CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT); - """) - - try oneConn.beginTransaction() - try oneConn.execute(sql: """ - INSERT INTO test (value) VALUES ('first'); - """) - - #expect( - throws: SQLiteError( - code: SQLITE_BUSY, - message: "database is locked" - ), - performing: { - twoConn.busyTimeout = 0 - try twoConn.execute(sql: """ - INSERT INTO test (value) VALUES ('second'); - """) - } - ) - - try oneConn.rollbackTransaction() - } - @Test func testApplicationID() throws { let connection = try Connection( - location: .inMemory, - options: [.create, .readwrite] + location: .inMemory, options: [.create, .readwrite] ) #expect(connection.applicationID == 0) @@ -133,8 +90,7 @@ struct ConnectionTests { @Test func testForeignKeys() throws { let connection = try Connection( - location: .inMemory, - options: [.create, .readwrite] + location: .inMemory, options: [.create, .readwrite] ) #expect(connection.foreignKeys == false) @@ -147,15 +103,12 @@ struct ConnectionTests { } @Test func testJournalMode() throws { - let url = FileManager.default.temporaryDirectory - .appendingPathComponent(UUID().uuidString) - .appendingPathExtension("sqlite") - defer { try? FileManager.default.removeItem(at: url) } + let dir = FileManager.default.temporaryDirectory + let file = UUID().uuidString + let path = dir.appending(component: file).path + defer { try? FileManager.default.removeItem(atPath: path) } - let connection = try Connection( - location: .file(path: url.path), - options: [.create, .readwrite] - ) + let connection = try Connection(path: path, options: [.create, .readwrite]) connection.journalMode = .delete #expect(try connection.get(pragma: .journalMode) == JournalMode.delete) @@ -166,8 +119,7 @@ struct ConnectionTests { @Test func testSynchronous() throws { let connection = try Connection( - location: .inMemory, - options: [.create, .readwrite] + location: .inMemory, options: [.create, .readwrite] ) connection.synchronous = .normal @@ -179,8 +131,7 @@ struct ConnectionTests { @Test func testUserVersion() throws { let connection = try Connection( - location: .inMemory, - options: [.create, .readwrite] + location: .inMemory, options: [.create, .readwrite] ) connection.userVersion = 42 @@ -190,88 +141,265 @@ struct ConnectionTests { #expect(connection.userVersion == 13) } - @Test(arguments: [ - (TestScalarFunc.self, TestScalarFunc.name), - (TestAggregateFunc.self, TestAggregateFunc.name) - ] as [(Function.Type, String)]) - func testAddFunction( - _ function: Function.Type, - _ name: String - ) throws { - let connection = try Connection( - location: .inMemory, - options: [.create, .readwrite] - ) - try connection.execute(sql: """ - CREATE TABLE items (value INTEGER); - INSERT INTO items (value) VALUES (1), (2), (NULL), (3); - """) - try connection.add(function: function) - try connection.execute(sql: "SELECT \(name)(value) FROM items") + @Test(arguments: ["main", nil]) + func applyKeyEncrypt(_ name: String?) throws { + let dir = FileManager.default.temporaryDirectory + let file = UUID().uuidString + let path = dir.appending(component: file).path + defer { try? FileManager.default.removeItem(atPath: path) } + + do { + let connection = try Connection(path: path, options: [.create, .readwrite]) + try connection.apply(.passphrase("test"), name: name) + try connection.execute(sql: "CREATE TABLE t (id INT PRIMARY KEY)") + } + + do { + var connection: OpaquePointer! + sqlite3_open_v2(path, &connection, SQLITE_OPEN_READONLY, nil) + let status = sqlite3_exec( + connection, "SELECT count(*) FROM sqlite_master", nil, nil, nil + ) + #expect(status == SQLITE_NOTADB) + } } - @Test(arguments: [ - (TestScalarFunc.self, TestScalarFunc.name), - (TestAggregateFunc.self, TestAggregateFunc.name) - ] as [(Function.Type, String)]) - func testRemoveFunction( - _ function: Function.Type, - _ name: String - ) throws { - let connection = try Connection( - location: .inMemory, - options: [.create, .readwrite] - ) - try connection.execute(sql: """ - CREATE TABLE items (value INTEGER); - INSERT INTO items (value) VALUES (1), (2), (NULL), (3); - """) - try connection.add(function: function) - try connection.remove(function: function) - #expect( - throws: SQLiteError( - code: SQLITE_ERROR, - message: "no such function: \(name)" - ), - performing: { - try connection.execute(sql: """ - SELECT \(name)(value) FROM items - """) + @Test(arguments: ["main", nil]) + func applyKeyDecrypt(_ name: String?) throws { + let dir = FileManager.default.temporaryDirectory + let file = UUID().uuidString + let path = dir.appending(component: file).path + defer { try? FileManager.default.removeItem(atPath: path) } + + do { + var connection: OpaquePointer! + let options = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE + sqlite3_open_v2(path, &connection, options, nil) + if let name { + sqlite3_key_v2(connection, name, "test", Int32("test".utf8.count)) + } else { + sqlite3_key(connection, "test", Int32("test".utf8.count)) } + sqlite3_exec( + connection, "CREATE TABLE t (id INT PRIMARY KEY)", nil, nil, nil + ) + sqlite3_close_v2(connection) + } + + do { + let connection = try Connection(path: path, options: [.readwrite]) + try connection.apply(.passphrase("test"), name: name) + try connection.execute(sql: "SELECT count(*) FROM sqlite_master") + } + } + + @Test(arguments: ["main", nil]) + func applyKeyInvalid(_ name: String?) throws { + let connection = try Connection( + location: .inMemory, options: [.create, .readwrite] ) + #expect( + throws: SQLiteError(code: SQLITE_MISUSE, message: ""), + performing: { try connection.apply(.passphrase(""), name: name) } + ) + } + + @Test(arguments: ["main", nil]) + func rekey(_ name: String?) throws { + let dir = FileManager.default.temporaryDirectory + let file = UUID().uuidString + let path = dir.appending(component: file).path + defer { try? FileManager.default.removeItem(atPath: path) } + + do { + let connection = try Connection(path: path, options: [.create, .readwrite]) + try connection.apply(.passphrase("old-test"), name: name) + try connection.execute(sql: "CREATE TABLE t (id INT PRIMARY KEY)") + } + + do { + let connection = try Connection(path: path, options: [.create, .readwrite]) + try connection.apply(.passphrase("old-test"), name: name) + try connection.rekey(.passphrase("new-test"), name: name) + } + + do { + let connection = try Connection(path: path, options: [.readwrite]) + try connection.apply(.passphrase("new-test"), name: name) + try connection.execute(sql: "SELECT count(*) FROM sqlite_master") + } + } + + @Test(arguments: ["main", nil]) + func rekeyInvalid(_ name: String?) throws { + let connection = try Connection( + location: .inMemory, options: [.create, .readwrite] + ) + try connection.apply(.passphrase("test"), name: name) + try connection.execute(sql: "CREATE TABLE t (id INT PRIMARY KEY)") + + #expect( + throws: SQLiteError(code: SQLITE_ERROR, message: ""), + performing: { try connection.rekey(.passphrase(""), name: name) } + ) + } + + @Test func addDelegate() throws { + let connection = try Connection( + location: .inMemory, options: [.create, .readwrite] + ) + try connection.execute(sql: "CREATE TABLE t (id INT PRIMARY KEY)") + + let delegate = ConnectionDelegate() + connection.add(delegate: delegate) + + try connection.execute(sql: "INSERT INTO t (id) VALUES (1)") + #expect(delegate.didUpdate) + #expect(delegate.willCommit) + #expect(delegate.didRollback == false) + + delegate.reset() + delegate.error = SQLiteError(code: -1, message: "") + + try? connection.execute(sql: "INSERT INTO t (id) VALUES (2)") + #expect(delegate.didUpdate) + #expect(delegate.willCommit) + #expect(delegate.didRollback) + + delegate.reset() + connection.remove(delegate: delegate) + + try connection.execute(sql: "INSERT INTO t (id) VALUES (3)") + #expect(delegate.didUpdate == false) + #expect(delegate.willCommit == false) + #expect(delegate.didRollback == false) + } + + @Test func addTraceDelegate() throws { + let connection = try Connection( + location: .inMemory, options: [.create, .readwrite] + ) + try connection.execute(sql: "CREATE TABLE t (id INT PRIMARY KEY)") + + let delegate = ConnectionTraceDelegate() + connection.add(trace: delegate) + + try connection.execute(sql: "INSERT INTO t (id) VALUES (:id)") + #expect(delegate.expandedSQL == "INSERT INTO t (id) VALUES (NULL)") + #expect(delegate.unexpandedSQL == "INSERT INTO t (id) VALUES (:id)") + + delegate.reset() + connection.remove(trace: delegate) + + try connection.execute(sql: "INSERT INTO t (id) VALUES (:id)") + #expect(delegate.expandedSQL == nil) + #expect(delegate.unexpandedSQL == nil) + } + + @Test func addFunction() throws { + let connection = try Connection( + location: .inMemory, options: [.create, .readwrite] + ) + + try connection.add(function: TestFunction.self) + #expect(TestFunction.isInstalled) + + try connection.remove(function: TestFunction.self) + #expect(TestFunction.isInstalled == false) + } + + @Test func beginTransaction() throws { + let connection = try Connection( + location: .inMemory, options: [.create, .readwrite] + ) + #expect(connection.isAutocommit) + + try connection.beginTransaction() + #expect(connection.isAutocommit == false) + } + + @Test func commitTransaction() throws { + let connection = try Connection( + location: .inMemory, options: [.create, .readwrite] + ) + #expect(connection.isAutocommit) + + try connection.beginTransaction() + try connection.commitTransaction() + #expect(connection.isAutocommit) + } + + @Test func rollbackTransaction() throws { + let connection = try Connection( + location: .inMemory, options: [.create, .readwrite] + ) + #expect(connection.isAutocommit) + + try connection.beginTransaction() + try connection.rollbackTransaction() + #expect(connection.isAutocommit) } } private extension ConnectionTests { - final class TestScalarFunc: Function.Scalar { - override class var argc: Int32 { 1 } - override class var name: String { "TO_STR" } - override class var options: Options { - [.deterministic, .innocuous] + final class ConnectionDelegate: DataLiteCore.ConnectionDelegate { + var error: Error? + + var didUpdate = false + var willCommit = false + var didRollback = false + + func reset() { + didUpdate = false + willCommit = false + didRollback = false } - override class func invoke(args: any ArgumentsProtocol) throws -> SQLiteRepresentable? { - args[0].description + func connection( + _ connection: any ConnectionProtocol, + didUpdate action: SQLiteAction + ) { + didUpdate = true + } + + func connectionWillCommit(_ connection: any ConnectionProtocol) throws { + willCommit = true + if let error { throw error } + } + + func connectionDidRollback(_ connection: any ConnectionProtocol) { + didRollback = true } } - final class TestAggregateFunc: Function.Aggregate { - override class var argc: Int32 { 1 } - override class var name: String { "MY_COUNT" } - override class var options: Options { - [.deterministic, .innocuous] + final class ConnectionTraceDelegate: DataLiteCore.ConnectionTraceDelegate { + var expandedSQL: String? + var unexpandedSQL: String? + + func reset() { + expandedSQL = nil + unexpandedSQL = nil } - private var count: Int = 0 + func connection(_ connection: any ConnectionProtocol, trace sql: Trace) { + expandedSQL = sql.expandedSQL + unexpandedSQL = sql.unexpandedSQL + } + } + + final class TestFunction: DataLiteCore.Function { + nonisolated(unsafe) static var isInstalled = false - override func step(args: any ArgumentsProtocol) throws { - if args[0] != .null { - count += 1 - } + override class func install( + db connection: OpaquePointer + ) throws(SQLiteError) { + isInstalled = true } - override func finalize() throws -> SQLiteRepresentable? { - count + override class func uninstall( + db connection: OpaquePointer + ) throws(SQLiteError) { + isInstalled = false } } } diff --git a/Tests/DataLiteCoreTests/Classes/Function+OptionsTests.swift b/Tests/DataLiteCoreTests/Classes/Function+OptionsTests.swift index 743c248..2813863 100644 --- a/Tests/DataLiteCoreTests/Classes/Function+OptionsTests.swift +++ b/Tests/DataLiteCoreTests/Classes/Function+OptionsTests.swift @@ -3,20 +3,20 @@ import DataLiteC import DataLiteCore struct FunctionOptionsTests { - @Test func testSingleOption() { + @Test func singleOption() { #expect(Function.Options.deterministic.rawValue == SQLITE_DETERMINISTIC) #expect(Function.Options.directonly.rawValue == SQLITE_DIRECTONLY) #expect(Function.Options.innocuous.rawValue == SQLITE_INNOCUOUS) } - @Test func testMultipleOptions() { + @Test func multipleOptions() { let options: Function.Options = [.deterministic, .directonly] #expect(options.contains(.deterministic)) #expect(options.contains(.directonly)) #expect(options.contains(.innocuous) == false) } - @Test func testEqualityAndHashability() { + @Test func equalityAndHashability() { let options1: Function.Options = [.deterministic, .innocuous] let options2: Function.Options = [.deterministic, .innocuous] #expect(options1 == options2) @@ -26,14 +26,14 @@ struct FunctionOptionsTests { #expect(hash1 == hash2) } - @Test func testEmptyOptions() { + @Test func emptyOptions() { let options = Function.Options(rawValue: 0) #expect(options.contains(.deterministic) == false) #expect(options.contains(.directonly) == false) #expect(options.contains(.innocuous) == false) } - @Test func testRawValueInitialization() { + @Test func rawValueInitialization() { let rawValue: Int32 = SQLITE_DETERMINISTIC | SQLITE_INNOCUOUS let options = Function.Options(rawValue: rawValue) @@ -42,7 +42,7 @@ struct FunctionOptionsTests { #expect(options.contains(.directonly) == false) } - @Test func testAddingAndRemovingOptions() { + @Test func addingAndRemovingOptions() { var options: Function.Options = [] options.insert(.deterministic) @@ -55,4 +55,3 @@ struct FunctionOptionsTests { #expect(options.contains(.deterministic) == false) } } - diff --git a/Tests/DataLiteCoreTests/Classes/Function+RegexpTests.swift b/Tests/DataLiteCoreTests/Classes/Function+RegexpTests.swift new file mode 100644 index 0000000..95fe920 --- /dev/null +++ b/Tests/DataLiteCoreTests/Classes/Function+RegexpTests.swift @@ -0,0 +1,82 @@ +import Foundation +import Testing +import DataLiteCore + +struct FunctionRegexpTests { + @Test func metadata() { + #expect(Regexp.argc == 2) + #expect(Regexp.name == "REGEXP") + #expect(Regexp.options == [.deterministic, .innocuous]) + } + + @Test func invalidArguments() { + let arguments: Arguments = [.int(1), .text("value")] + #expect( + performing: { + try Regexp.invoke(args: arguments) + }, + throws: { + switch $0 { + case Regexp.Error.invalidArguments: + return true + default: + return false + } + } + ) + } + + @Test func invalidPattern() { + let arguments: Arguments = [.text("("), .text("value")] + #expect( + performing: { + try Regexp.invoke(args: arguments) + }, + throws: { + switch $0 { + case Regexp.Error.regexError: + return true + default: + return false + } + } + ) + } + + @Test func matchesPattern() throws { + let arguments: Arguments = [.text("foo.*"), .text("foobar")] + #expect(try Regexp.invoke(args: arguments) as? Bool == true) + } + + @Test func doesNotMatchPattern() throws { + let arguments: Arguments = [.text("bar.*"), .text("foobar")] + #expect(try Regexp.invoke(args: arguments) as? Bool == false) + } +} + +private extension FunctionRegexpTests { + typealias Regexp = Function.Regexp + + struct Arguments: ArgumentsProtocol, ExpressibleByArrayLiteral { + private let values: [SQLiteValue] + + var startIndex: Int { values.startIndex } + var endIndex: Int { values.endIndex } + + init(_ values: [SQLiteValue]) { + self.values = values + } + + init(arrayLiteral elements: SQLiteValue...) { + self.values = elements + } + + subscript(index: Int) -> SQLiteValue { + values[index] + } + + func index(after i: Int) -> Int { + values.index(after: i) + } + } +} diff --git a/Tests/DataLiteCoreTests/Classes/Statement+OptionsTests.swift b/Tests/DataLiteCoreTests/Classes/Statement+OptionsTests.swift index 081e3b4..5b31f8e 100644 --- a/Tests/DataLiteCoreTests/Classes/Statement+OptionsTests.swift +++ b/Tests/DataLiteCoreTests/Classes/Statement+OptionsTests.swift @@ -4,15 +4,15 @@ import DataLiteC import DataLiteCore struct StatementOptionsTests { - @Test func testPersistentOptions() { + @Test func persistentOptions() { #expect(Statement.Options.persistent.rawValue == UInt32(SQLITE_PREPARE_PERSISTENT)) } - @Test func testNoVtabOptions() { + @Test func noVtabOptions() { #expect(Statement.Options.noVtab.rawValue == UInt32(SQLITE_PREPARE_NO_VTAB)) } - @Test func testCombineOptions() { + @Test func combineOptions() { let options: Statement.Options = [.persistent, .noVtab] let expected = UInt32(SQLITE_PREPARE_PERSISTENT | SQLITE_PREPARE_NO_VTAB) #expect(options.contains(.persistent)) @@ -20,19 +20,19 @@ struct StatementOptionsTests { #expect(options.rawValue == expected) } - @Test func testInitWithUInt32RawValue() { + @Test func initWithUInt32RawValue() { let raw = UInt32(SQLITE_PREPARE_PERSISTENT) let options = Statement.Options(rawValue: raw) #expect(options == .persistent) } - @Test func testInitWithInt32RawValue() { + @Test func initWithInt32RawValue() { let raw = Int32(SQLITE_PREPARE_NO_VTAB) let options = Statement.Options(rawValue: raw) #expect(options == .noVtab) } - @Test func testEmptySetRawValueIsZero() { + @Test func emptySetRawValueIsZero() { let empty: Statement.Options = [] #expect(empty.rawValue == 0) #expect(!empty.contains(.persistent)) diff --git a/Tests/DataLiteCoreTests/Classes/StatementTests.swift b/Tests/DataLiteCoreTests/Classes/StatementTests.swift index c8e3860..629525b 100644 --- a/Tests/DataLiteCoreTests/Classes/StatementTests.swift +++ b/Tests/DataLiteCoreTests/Classes/StatementTests.swift @@ -30,7 +30,7 @@ final class StatementTests { sqlite3_close_v2(connection) } - @Test func testInitWithError() throws { + @Test func initWithError() throws { #expect( throws: SQLiteError( code: SQLITE_ERROR, @@ -46,19 +46,22 @@ final class StatementTests { ) } - @Test func testParameterCount() throws { - let sql = "SELECT * FROM t WHERE id = ? AND s = ?" + @Test func sqlString() throws { + let sql = "SELECT * FROM t WHERE id = ?" let stmt = try Statement(db: connection, sql: sql, options: []) - #expect(stmt.parameterCount() == 2) + #expect(stmt.sql == sql) } - @Test func testZeroParameterCount() throws { - let sql = "SELECT * FROM t" + @Test(arguments: [ + ("SELECT * FROM t WHERE id = ? AND s = ?", 2), + ("SELECT * FROM t WHERE id = 1 AND s = ''", 0) + ]) + func parameterCount(_ sql: String, _ expanded: Int32) throws { let stmt = try Statement(db: connection, sql: sql, options: []) - #expect(stmt.parameterCount() == 0) + #expect(stmt.parameterCount() == expanded) } - @Test func testParameterIndexByName() throws { + @Test func parameterIndexByName() throws { let sql = "SELECT * FROM t WHERE id = :id AND s = :s" let stmt = try Statement(db: connection, sql: sql, options: []) #expect(stmt.parameterIndexBy(":id") == 1) @@ -66,7 +69,7 @@ final class StatementTests { #expect(stmt.parameterIndexBy(":invalid") == 0) } - @Test func testParameterNameByIndex() throws { + @Test func parameterNameByIndex() throws { let sql = "SELECT * FROM t WHERE id = :id AND s = :s" let stmt = try Statement(db: connection, sql: sql, options: []) #expect(stmt.parameterNameBy(1) == ":id") @@ -74,20 +77,36 @@ final class StatementTests { #expect(stmt.parameterNameBy(3) == nil) } - @Test func testBindValueAtIndex() throws { - let sql = "SELECT * FROM t where id = ?" + @Test func bindValueAtIndex() throws { + let sql = "SELECT * FROM t WHERE id = ?" + let stmt = try Statement(db: connection, sql: sql, options: []) + #expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = NULL") + try stmt.bind(.int(42), at: 1) + #expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = 42") + try stmt.bind(.real(42), at: 1) + #expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = 42.0") + try stmt.bind(.text("42"), at: 1) + #expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = '42'") + try stmt.bind(.blob(Data([0x42])), at: 1) + #expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = x'42'") + try stmt.bind(.null, at: 1) + #expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = NULL") + try stmt.bind(TestValue(value: 42), at: 1) + #expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = 42") + try stmt.bind(TestValue?.none, at: 1) + #expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = NULL") } - @Test func testErrorBindValueAtIndex() throws { - let sql = "SELECT * FROM t where id = ?" + @Test func errorBindValueAtIndex() throws { + let sql = "SELECT * FROM t WHERE id = ?" let stmt = try Statement(db: connection, sql: sql, options: []) #expect( throws: SQLiteError( @@ -100,20 +119,36 @@ final class StatementTests { ) } - @Test func testBindValueByName() throws { - let sql = "SELECT * FROM t where id = :id" + @Test func bindValueByName() throws { + let sql = "SELECT * FROM t WHERE id = :id" + let stmt = try Statement(db: connection, sql: sql, options: []) + #expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = NULL") + try stmt.bind(.int(42), by: ":id") + #expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = 42") + try stmt.bind(.real(42), by: ":id") + #expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = 42.0") + try stmt.bind(.text("42"), by: ":id") + #expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = '42'") + try stmt.bind(.blob(Data([0x42])), by: ":id") + #expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = x'42'") + try stmt.bind(.null, by: ":id") + #expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = NULL") + try stmt.bind(TestValue(value: 42), by: ":id") + #expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = 42") + try stmt.bind(TestValue?.none, by: ":id") + #expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = NULL") } - @Test func testErrorBindValueByName() throws { - let sql = "SELECT * FROM t where id = :id" + @Test func errorBindValueByName() throws { + let sql = "SELECT * FROM t WHERE id = :id" let stmt = try Statement(db: connection, sql: sql, options: []) #expect( throws: SQLiteError( @@ -126,14 +161,52 @@ final class StatementTests { ) } - @Test func testStepOneRow() throws { - let sql = "SELECT 1 where 1" + @Test func bindRow() throws { + let row: SQLiteRow = ["id": .int(42), "name": .text("Alice")] + let sql = "SELECT * FROM t WHERE id = :id AND s = :name" + + let stmt = try Statement(db: connection, sql: sql, options: []) + #expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = NULL AND s = NULL") + + try stmt.bind(row) + #expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = 42 AND s = 'Alice'") + } + + @Test func errorBindRow() throws { + let row: SQLiteRow = ["name": .text("Alice")] + let stmt = try Statement( + db: connection, sql: "SELECT * FROM t", options: [] + ) + #expect( + throws: SQLiteError( + code: SQLITE_RANGE, + message: "column index out of range" + ), + performing: { + try stmt.bind(row) + } + ) + } + + @Test func clearBindings() throws { + let sql = "SELECT * FROM t WHERE id = :id" + let stmt = try Statement(db: connection, sql: sql, options: []) + + try stmt.bind(.int(42), at: 1) + #expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = 42") + + try stmt.clearBindings() + #expect(stmt.expandedSQL == "SELECT * FROM t WHERE id = NULL") + } + + @Test func stepOneRow() throws { + let sql = "SELECT 1 WHERE 1" let stmt = try Statement(db: connection, sql: sql, options: []) #expect(try stmt.step()) #expect(try stmt.step() == false) } - @Test func testStepMultipleRows() throws { + @Test func stepMultipleRows() throws { sqlite3_exec(connection, "INSERT INTO t(n) VALUES (1),(2),(3)", nil, nil, nil) let sql = "SELECT id FROM t ORDER BY id" let stmt = try Statement(db: connection, sql: sql, options: []) @@ -143,13 +216,13 @@ final class StatementTests { #expect(try stmt.step() == false) } - @Test func testStepNoRows() throws { + @Test func stepNoRows() throws { let sql = "SELECT 1 WHERE 0" let stmt = try Statement(db: connection, sql: sql, options: []) #expect(try stmt.step() == false) } - @Test func testStepWithError() throws { + @Test func stepWithError() throws { sqlite3_exec(connection, "INSERT INTO t(id, n) VALUES (1, 10)", nil, nil, nil) let sql = "INSERT INTO t(id, n) VALUES (?, ?)" let stmt = try Statement(db: connection, sql: sql, options: []) @@ -166,13 +239,52 @@ final class StatementTests { ) } - @Test func testColumnCount() throws { + @Test func executeRows() throws { + let rows: [SQLiteRow] = [ + [ + "id": .int(1), + "n": .int(42), + "r": .real(3.14), + "s": .text("Test"), + "b": .blob(Data([0x42])) + ], + [ + "id": .int(2), + "n": .null, + "r": .null, + "s": .null, + "b": .null + ] + ] + let sql = "INSERT INTO t(id, n, r, s, b) VALUES (:id, :n, :r, :s, :b)" + try Statement(db: connection, sql: sql, options: []).execute(rows) + + let stmt = try Statement(db: connection, sql: "SELECT * FROM t", options: []) + + #expect(try stmt.step()) + #expect(stmt.currentRow() == rows[0]) + + #expect(try stmt.step()) + #expect(stmt.currentRow() == rows[1]) + + #expect(try stmt.step() == false) + } + + @Test func executeEmptyRows() throws { + let sql = "INSERT INTO t(id, n, r, s, b) VALUES (:id, :n, :r, :s, :b)" + try Statement(db: connection, sql: sql, options: []).execute([]) + + let stmt = try Statement(db: connection, sql: "SELECT * FROM t", options: []) + #expect(try stmt.step() == false) + } + + @Test func columnCount() throws { let sql = "SELECT * FROM t" let stmt = try Statement(db: connection, sql: sql, options: []) #expect(stmt.columnCount() == 5) } - @Test func testColumnName() throws { + @Test func columnName() throws { let sql = "SELECT * FROM t" let stmt = try Statement(db: connection, sql: sql, options: []) #expect(stmt.columnName(at: 0) == "id") @@ -180,10 +292,13 @@ final class StatementTests { #expect(stmt.columnName(at: 2) == "r") #expect(stmt.columnName(at: 3) == "s") #expect(stmt.columnName(at: 4) == "b") + #expect(stmt.columnName(at: 5) == nil) } - @Test func testColumnValueAtIndex() throws { - sqlite3_exec(connection, """ + @Test func columnValueAtIndex() throws { + sqlite3_exec( + connection, + """ INSERT INTO t (id, n, r, s, b) VALUES (10, 42, 3.5, 'hello', x'DEADBEEF') """, nil, nil, nil @@ -201,8 +316,10 @@ final class StatementTests { #expect(stmt.columnValue(at: 4) == .blob(Data([0xDE, 0xAD, 0xBE, 0xEF]))) } - @Test func testColumnNullValueAtIndex() throws { - sqlite3_exec(connection, """ + @Test func columnNullValueAtIndex() throws { + sqlite3_exec( + connection, + """ INSERT INTO t (id) VALUES (10) """, nil, nil, nil ) @@ -215,6 +332,30 @@ final class StatementTests { #expect(stmt.columnValue(at: 1) == .null) #expect(stmt.columnValue(at: 1) == TestValue?.none) } + + @Test func currentRow() throws { + sqlite3_exec( + connection, + """ + INSERT INTO t (id, n, r, s, b) + VALUES (10, 42, 3.5, 'hello', x'DEADBEEF') + """, nil, nil, nil + ) + + let row: SQLiteRow = [ + "id": .int(10), + "n": .int(42), + "r": .real(3.5), + "s": .text("hello"), + "b": .blob(Data([0xDE, 0xAD, 0xBE, 0xEF])) + ] + + let sql = "SELECT * FROM t WHERE id = 10" + let stmt = try Statement(db: connection, sql: sql, options: []) + + #expect(try stmt.step()) + #expect(stmt.currentRow() == row) + } } private extension StatementTests { diff --git a/Tests/DataLiteCoreTests/Enums/JournalModeTests.swift b/Tests/DataLiteCoreTests/Enums/JournalModeTests.swift new file mode 100644 index 0000000..f23dfce --- /dev/null +++ b/Tests/DataLiteCoreTests/Enums/JournalModeTests.swift @@ -0,0 +1,33 @@ +import Testing +import DataLiteCore + +struct JournalModeTests { + @Test func rawValue() { + #expect(JournalMode.delete.rawValue == "DELETE") + #expect(JournalMode.truncate.rawValue == "TRUNCATE") + #expect(JournalMode.persist.rawValue == "PERSIST") + #expect(JournalMode.memory.rawValue == "MEMORY") + #expect(JournalMode.wal.rawValue == "WAL") + #expect(JournalMode.off.rawValue == "OFF") + } + + @Test func initRawValue() { + #expect(JournalMode(rawValue: "DELETE") == .delete) + #expect(JournalMode(rawValue: "delete") == .delete) + + #expect(JournalMode(rawValue: "TRUNCATE") == .truncate) + #expect(JournalMode(rawValue: "truncate") == .truncate) + + #expect(JournalMode(rawValue: "PERSIST") == .persist) + #expect(JournalMode(rawValue: "persist") == .persist) + + #expect(JournalMode(rawValue: "MEMORY") == .memory) + #expect(JournalMode(rawValue: "memory") == .memory) + + #expect(JournalMode(rawValue: "WAL") == .wal) + #expect(JournalMode(rawValue: "wal") == .wal) + + #expect(JournalMode(rawValue: "OFF") == .off) + #expect(JournalMode(rawValue: "off") == .off) + } +} diff --git a/Tests/DataLiteCoreTests/Enums/SQLiteActionTests.swift b/Tests/DataLiteCoreTests/Enums/SQLiteActionTests.swift deleted file mode 100644 index 5675f80..0000000 --- a/Tests/DataLiteCoreTests/Enums/SQLiteActionTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -import XCTest -import DataLiteCore - -class SQLiteActionTests: XCTestCase { - func testInsertAction() { - let action = SQLiteAction.insert(db: "testDB", table: "users", rowID: 1) - - switch action { - case .insert(let db, let table, let rowID): - XCTAssertEqual(db, "testDB", "Database name should be 'testDB'") - XCTAssertEqual(table, "users", "Table name should be 'users'") - XCTAssertEqual(rowID, 1, "Row ID should be 1") - default: - XCTFail("Expected insert action") - } - } - - func testUpdateAction() { - let action = SQLiteAction.update(db: "testDB", table: "users", rowID: 1) - - switch action { - case .update(let db, let table, let rowID): - XCTAssertEqual(db, "testDB", "Database name should be 'testDB'") - XCTAssertEqual(table, "users", "Table name should be 'users'") - XCTAssertEqual(rowID, 1, "Row ID should be 1") - default: - XCTFail("Expected update action") - } - } - - func testDeleteAction() { - let action = SQLiteAction.delete(db: "testDB", table: "users", rowID: 1) - - switch action { - case .delete(let db, let table, let rowID): - XCTAssertEqual(db, "testDB", "Database name should be 'testDB'") - XCTAssertEqual(table, "users", "Table name should be 'users'") - XCTAssertEqual(rowID, 1, "Row ID should be 1") - default: - XCTFail("Expected delete action") - } - } -} diff --git a/Tests/DataLiteCoreTests/Enums/SQLiteValueTests.swift b/Tests/DataLiteCoreTests/Enums/SQLiteValueTests.swift index edb7c76..435cc56 100644 --- a/Tests/DataLiteCoreTests/Enums/SQLiteValueTests.swift +++ b/Tests/DataLiteCoreTests/Enums/SQLiteValueTests.swift @@ -4,14 +4,14 @@ import DataLiteCore struct SQLiteValueTests { @Test(arguments: [1, 42, 1234]) - func testSQLiteIntValue(_ value: Int64) { + func intSQLiteValue(_ value: Int64) { let value = SQLiteValue.int(value) #expect(value.sqliteLiteral == "\(value)") #expect(value.description == value.sqliteLiteral) } @Test(arguments: [12, 0.5, 123.99]) - func testSQLiteRealValue(_ value: Double) { + func realSQLiteValue(_ value: Double) { let value = SQLiteValue.real(value) #expect(value.sqliteLiteral == "\(value)") #expect(value.description == value.sqliteLiteral) @@ -24,7 +24,7 @@ struct SQLiteValueTests { ("O'Reilly", "'O''Reilly'"), ("It's John's \"book\"", "'It''s John''s \"book\"'") ]) - func testSQLiteTextValue(_ value: String, _ expected: String) { + func textSQLiteValue(_ value: String, _ expected: String) { let value = SQLiteValue.text(value) #expect(value.sqliteLiteral == expected) #expect(value.description == value.sqliteLiteral) @@ -35,13 +35,13 @@ struct SQLiteValueTests { (Data([0x00]), "X'00'"), (Data([0x00, 0xAB, 0xCD]), "X'00ABCD'") ]) - func testSQLiteBlobValue(_ value: Data, _ expected: String) { + func blobSQLiteValue(_ value: Data, _ expected: String) { let value = SQLiteValue.blob(value) #expect(value.sqliteLiteral == expected) #expect(value.description == value.sqliteLiteral) } - @Test func testSQLiteNullValue() { + @Test func nullSQLiteValue() { let value = SQLiteValue.null #expect(value.sqliteLiteral == "NULL") #expect(value.description == value.sqliteLiteral) diff --git a/Tests/DataLiteCoreTests/Enums/TransactionTypeTests.swift b/Tests/DataLiteCoreTests/Enums/TransactionTypeTests.swift new file mode 100644 index 0000000..0dc499a --- /dev/null +++ b/Tests/DataLiteCoreTests/Enums/TransactionTypeTests.swift @@ -0,0 +1,29 @@ +import Testing +import DataLiteCore + +struct TransactionTypeTests { + @Test func description() { + #expect(TransactionType.deferred.description == "DEFERRED") + #expect(TransactionType.immediate.description == "IMMEDIATE") + #expect(TransactionType.exclusive.description == "EXCLUSIVE") + } + + @Test func rawValue() { + #expect(TransactionType.deferred.rawValue == "DEFERRED") + #expect(TransactionType.immediate.rawValue == "IMMEDIATE") + #expect(TransactionType.exclusive.rawValue == "EXCLUSIVE") + } + + @Test func initRawValue() { + #expect(TransactionType(rawValue: "DEFERRED") == .deferred) + #expect(TransactionType(rawValue: "deferred") == .deferred) + + #expect(TransactionType(rawValue: "IMMEDIATE") == .immediate) + #expect(TransactionType(rawValue: "immediate") == .immediate) + + #expect(TransactionType(rawValue: "EXCLUSIVE") == .exclusive) + #expect(TransactionType(rawValue: "exclusive") == .exclusive) + + #expect(TransactionType(rawValue: "SOME_STR") == nil) + } +} diff --git a/Tests/DataLiteCoreTests/Extensions/BinaryFloatingPointTests.swift b/Tests/DataLiteCoreTests/Extensions/BinaryFloatingPointTests.swift index f793299..bc79f69 100644 --- a/Tests/DataLiteCoreTests/Extensions/BinaryFloatingPointTests.swift +++ b/Tests/DataLiteCoreTests/Extensions/BinaryFloatingPointTests.swift @@ -1,50 +1,26 @@ +import Foundation import Testing import DataLiteCore struct BinaryFloatingPointTests { - @Test func testFloatToSQLiteRawValue() { - let floatValue: Float = 3.14 - let rawValue = floatValue.sqliteValue - #expect(rawValue == .real(Double(floatValue))) + @Test func floatingPointToSQLiteValue() { + #expect(Float(3.14).sqliteValue == .real(Double(Float(3.14)))) + #expect(Double(3.14).sqliteValue == .real(3.14)) } - @Test func testDoubleToSQLiteRawValue() { - let doubleValue: Double = 3.14 - let rawValue = doubleValue.sqliteValue - #expect(rawValue == .real(doubleValue)) - } - - @Test func testFloatInitializationFromSQLiteRawValue() { - let realValue: SQLiteValue = .real(3.14) - let floatValue = Float(realValue) - #expect(floatValue != nil) - #expect(floatValue == 3.14) + @Test func floatingPointFromSQLiteValue() { + #expect(Float(SQLiteValue.real(3.14)) == 3.14) + #expect(Float(SQLiteValue.int(42)) == 42) - let intValue: SQLiteValue = .int(42) - let floatFromInt = Float(intValue) - #expect(floatFromInt != nil) - #expect(floatFromInt == 42.0) - } - - @Test func testDoubleInitializationFromSQLiteRawValue() { - let realValue: SQLiteValue = .real(3.14) - let doubleValue = Double(realValue) - #expect(doubleValue != nil) - #expect(doubleValue == 3.14) + #expect(Double(SQLiteValue.real(3.14)) == 3.14) + #expect(Double(SQLiteValue.int(42)) == 42) - let intValue: SQLiteValue = .int(42) - let doubleFromInt = Double(intValue) - #expect(doubleFromInt != nil) - #expect(doubleFromInt == 42.0) - } - - @Test func testInitializationFailureFromInvalidSQLiteRawValue() { - let nullValue: SQLiteValue = .null - #expect(Float(nullValue) == nil) - #expect(Double(nullValue) == nil) + #expect(Float(SQLiteValue.text("42")) == nil) + #expect(Float(SQLiteValue.blob(Data([0x42]))) == nil) + #expect(Float(SQLiteValue.null) == nil) - let textValue: SQLiteValue = .text("Invalid") - #expect(Float(textValue) == nil) - #expect(Double(textValue) == nil) + #expect(Double(SQLiteValue.text("42")) == nil) + #expect(Double(SQLiteValue.blob(Data([0x42]))) == nil) + #expect(Double(SQLiteValue.null) == nil) } } diff --git a/Tests/DataLiteCoreTests/Extensions/BinaryIntegerTests.swift b/Tests/DataLiteCoreTests/Extensions/BinaryIntegerTests.swift index 935851f..1fbeeee 100644 --- a/Tests/DataLiteCoreTests/Extensions/BinaryIntegerTests.swift +++ b/Tests/DataLiteCoreTests/Extensions/BinaryIntegerTests.swift @@ -3,7 +3,7 @@ import Foundation import DataLiteCore struct BinaryIntegerTests { - @Test func testIntegerToSQLiteValue() { + @Test func integerToSQLiteValue() { #expect(Int(42).sqliteValue == .int(42)) #expect(Int8(42).sqliteValue == .int(42)) #expect(Int16(42).sqliteValue == .int(42)) @@ -17,7 +17,7 @@ struct BinaryIntegerTests { #expect(UInt64(42).sqliteValue == .int(42)) } - @Test func testIntegerInitializationFromSQLiteValue() { + @Test func integerFromSQLiteValue() { #expect(Int(SQLiteValue.int(42)) == 42) #expect(Int8(SQLiteValue.int(42)) == 42) #expect(Int16(SQLiteValue.int(42)) == 42) @@ -29,15 +29,61 @@ struct BinaryIntegerTests { #expect(UInt16(SQLiteValue.int(42)) == 42) #expect(UInt32(SQLiteValue.int(42)) == 42) #expect(UInt64(SQLiteValue.int(42)) == 42) - } - - @Test func testInvalidIntegerInitialization() { - #expect(Int(SQLiteValue.real(3.14)) == nil) - #expect(Int8(SQLiteValue.text("test")) == nil) - #expect(UInt32(SQLiteValue.blob(Data([0x01, 0x02]))) == nil) - // Out-of-range conversion - let largeValue = Int64.max - #expect(Int8(exactly: largeValue) == nil) + #expect(Int(SQLiteValue.real(42)) == nil) + #expect(Int(SQLiteValue.text("42")) == nil) + #expect(Int(SQLiteValue.blob(Data([0x42]))) == nil) + #expect(Int(SQLiteValue.null) == nil) + + #expect(Int8(SQLiteValue.real(42)) == nil) + #expect(Int8(SQLiteValue.text("42")) == nil) + #expect(Int8(SQLiteValue.blob(Data([0x42]))) == nil) + #expect(Int8(SQLiteValue.null) == nil) + #expect(Int8(SQLiteValue.int(Int64.max)) == nil) + + #expect(Int16(SQLiteValue.real(42)) == nil) + #expect(Int16(SQLiteValue.text("42")) == nil) + #expect(Int16(SQLiteValue.blob(Data([0x42]))) == nil) + #expect(Int16(SQLiteValue.null) == nil) + #expect(Int16(SQLiteValue.int(Int64.max)) == nil) + + #expect(Int32(SQLiteValue.real(42)) == nil) + #expect(Int32(SQLiteValue.text("42")) == nil) + #expect(Int32(SQLiteValue.blob(Data([0x42]))) == nil) + #expect(Int32(SQLiteValue.null) == nil) + #expect(Int32(SQLiteValue.int(Int64.max)) == nil) + + #expect(Int64(SQLiteValue.real(42)) == nil) + #expect(Int64(SQLiteValue.text("42")) == nil) + #expect(Int64(SQLiteValue.blob(Data([0x42]))) == nil) + #expect(Int64(SQLiteValue.null) == nil) + + #expect(UInt(SQLiteValue.real(42)) == nil) + #expect(UInt(SQLiteValue.text("42")) == nil) + #expect(UInt(SQLiteValue.blob(Data([0x42]))) == nil) + #expect(UInt(SQLiteValue.null) == nil) + + #expect(UInt8(SQLiteValue.real(42)) == nil) + #expect(UInt8(SQLiteValue.text("42")) == nil) + #expect(UInt8(SQLiteValue.blob(Data([0x42]))) == nil) + #expect(UInt8(SQLiteValue.null) == nil) + #expect(UInt8(SQLiteValue.int(Int64.max)) == nil) + + #expect(UInt16(SQLiteValue.real(42)) == nil) + #expect(UInt16(SQLiteValue.text("42")) == nil) + #expect(UInt16(SQLiteValue.blob(Data([0x42]))) == nil) + #expect(UInt16(SQLiteValue.null) == nil) + #expect(UInt16(SQLiteValue.int(Int64.max)) == nil) + + #expect(UInt32(SQLiteValue.real(42)) == nil) + #expect(UInt32(SQLiteValue.text("42")) == nil) + #expect(UInt32(SQLiteValue.blob(Data([0x42]))) == nil) + #expect(UInt32(SQLiteValue.null) == nil) + #expect(UInt32(SQLiteValue.int(Int64.max)) == nil) + + #expect(UInt64(SQLiteValue.real(42)) == nil) + #expect(UInt64(SQLiteValue.text("42")) == nil) + #expect(UInt64(SQLiteValue.blob(Data([0x42]))) == nil) + #expect(UInt64(SQLiteValue.null) == nil) } } diff --git a/Tests/DataLiteCoreTests/Extensions/BoolTests.swift b/Tests/DataLiteCoreTests/Extensions/BoolTests.swift index e77d055..c56ef2a 100644 --- a/Tests/DataLiteCoreTests/Extensions/BoolTests.swift +++ b/Tests/DataLiteCoreTests/Extensions/BoolTests.swift @@ -3,20 +3,20 @@ import Foundation import DataLiteCore struct BoolTests { - @Test func testBoolToSQLiteRawValue() { + @Test func boolToSQLiteValue() { #expect(true.sqliteValue == .int(1)) #expect(false.sqliteValue == .int(0)) } - @Test func testSQLiteRawValueToBool() { - #expect(Bool(.int(1)) == true) - #expect(Bool(.int(0)) == false) + @Test func boolFromSQLiteValue() { + #expect(Bool(SQLiteValue.int(1)) == true) + #expect(Bool(SQLiteValue.int(0)) == false) - #expect(Bool(.int(-1)) == nil) - #expect(Bool(.int(2)) == nil) - #expect(Bool(.real(1.0)) == nil) - #expect(Bool(.text("true")) == nil) - #expect(Bool(.blob(Data())) == nil) - #expect(Bool(.null) == nil) + #expect(Bool(SQLiteValue.int(-1)) == nil) + #expect(Bool(SQLiteValue.int(2)) == nil) + #expect(Bool(SQLiteValue.real(1.0)) == nil) + #expect(Bool(SQLiteValue.text("true")) == nil) + #expect(Bool(SQLiteValue.blob(Data())) == nil) + #expect(Bool(SQLiteValue.null) == nil) } } diff --git a/Tests/DataLiteCoreTests/Extensions/DataTests.swift b/Tests/DataLiteCoreTests/Extensions/DataTests.swift index 14f6ad7..2019c40 100644 --- a/Tests/DataLiteCoreTests/Extensions/DataTests.swift +++ b/Tests/DataLiteCoreTests/Extensions/DataTests.swift @@ -3,20 +3,18 @@ import Foundation import DataLiteCore struct DataSQLiteRawRepresentableTests { - @Test func testDataToSQLiteRawValue() { + @Test func dataToSQLiteValue() { let data = Data([0x01, 0x02, 0x03]) #expect(data.sqliteValue == .blob(data)) } - @Test func testSQLiteRawValueToData() { + @Test func dataFromSQLiteValue() { let data = Data([0x01, 0x02, 0x03]) - let rawValue = SQLiteValue.blob(data) + #expect(Data(SQLiteValue.blob(data)) == data) - #expect(Data(rawValue) == data) - - #expect(Data(.int(1)) == nil) - #expect(Data(.real(1.0)) == nil) - #expect(Data(.text("blob")) == nil) - #expect(Data(.null) == nil) + #expect(Data(SQLiteValue.int(1)) == nil) + #expect(Data(SQLiteValue.real(1.0)) == nil) + #expect(Data(SQLiteValue.text("blob")) == nil) + #expect(Data(SQLiteValue.null) == nil) } } diff --git a/Tests/DataLiteCoreTests/Extensions/DateTests.swift b/Tests/DataLiteCoreTests/Extensions/DateTests.swift index 667027f..7ff6d72 100644 --- a/Tests/DataLiteCoreTests/Extensions/DateTests.swift +++ b/Tests/DataLiteCoreTests/Extensions/DateTests.swift @@ -3,30 +3,23 @@ import Foundation import DataLiteCore struct DateSQLiteRawRepresentableTests { - @Test func testDateToSQLiteRawValue() { + @Test func dateToSQLiteValue() { let date = Date(timeIntervalSince1970: 1609459200) - let formatter = ISO8601DateFormatter() - let dateString = formatter.string(from: date) + let dateString = "2021-01-01T00:00:00Z" #expect(date.sqliteValue == .text(dateString)) } - @Test func testSQLiteRawValueToDate() { + @Test func dateFromSQLiteValue() { let date = Date(timeIntervalSince1970: 1609459200) - let formatter = ISO8601DateFormatter() - let dateString = formatter.string(from: date) + let dateString = "2021-01-01T00:00:00Z" - let rawText = SQLiteValue.text(dateString) - #expect(Date(rawText) == date) + #expect(Date(SQLiteValue.text(dateString)) == date) + #expect(Date(SQLiteValue.int(1609459200)) == date) + #expect(Date(SQLiteValue.real(1609459200)) == date) - let rawInt = SQLiteValue.int(1609459200) - #expect(Date(rawInt) == date) - - let rawReal = SQLiteValue.real(1609459200) - #expect(Date(rawReal) == date) - - #expect(Date(.blob(Data([0x01, 0x02, 0x03]))) == nil) - #expect(Date(.null) == nil) - #expect(Date(.text("Invalid date format")) == nil) + #expect(Date(SQLiteValue.blob(Data([0x01, 0x02, 0x03]))) == nil) + #expect(Date(SQLiteValue.null) == nil) + #expect(Date(SQLiteValue.text("Invalid date format")) == nil) } } diff --git a/Tests/DataLiteCoreTests/Extensions/RawRepresentableTests.swift b/Tests/DataLiteCoreTests/Extensions/RawRepresentableTests.swift index a01a2dd..e6589bd 100644 --- a/Tests/DataLiteCoreTests/Extensions/RawRepresentableTests.swift +++ b/Tests/DataLiteCoreTests/Extensions/RawRepresentableTests.swift @@ -3,23 +3,30 @@ import Foundation import DataLiteCore struct RawRepresentableTests { - @Test func testRawRepresentableToSQLiteRawValue() { - let color: Color = .green - #expect(color.sqliteValue == .int(1)) + @Test func rawRepresentableToSQLiteValue() { + #expect(Color.red.sqliteValue == .int(Color.red.rawValue)) + #expect(Color.green.sqliteValue == .int(Color.green.rawValue)) + #expect(Color.blue.sqliteValue == .int(Color.blue.rawValue)) } - @Test func testSQLiteRawValueToRawRepresentable() { - #expect(Color(.int(2)) == .blue) + @Test func rawRepresentableFromSQLiteValue() { + #expect(Color(SQLiteValue.int(0)) == .red) + #expect(Color(SQLiteValue.int(1)) == .green) + #expect(Color(SQLiteValue.int(2)) == .blue) - #expect(Color(.int(42)) == nil) - #expect(Color(.text("red")) == nil) - #expect(Color(.blob(Data([0x01, 0x02]))) == nil) - #expect(Color(.null) == nil) + #expect(Color(SQLiteValue.int(42)) == nil) + #expect(Color(SQLiteValue.real(0)) == nil) + #expect(Color(SQLiteValue.real(1)) == nil) + #expect(Color(SQLiteValue.real(2)) == nil) + #expect(Color(SQLiteValue.real(42)) == nil) + #expect(Color(SQLiteValue.text("red")) == nil) + #expect(Color(SQLiteValue.blob(Data([0x01, 0x02]))) == nil) + #expect(Color(SQLiteValue.null) == nil) } } private extension RawRepresentableTests { - enum Color: Int, SQLiteRepresentable { + enum Color: Int64, SQLiteRepresentable { case red case green case blue diff --git a/Tests/DataLiteCoreTests/Extensions/StringTests.swift b/Tests/DataLiteCoreTests/Extensions/StringTests.swift index 85c2a7f..1d64372 100644 --- a/Tests/DataLiteCoreTests/Extensions/StringTests.swift +++ b/Tests/DataLiteCoreTests/Extensions/StringTests.swift @@ -3,15 +3,16 @@ import Foundation import DataLiteCore struct StringTests { - @Test func testStringToSQLiteRawValue() { + @Test func stringToSQLiteValue() { #expect("Hello, SQLite!".sqliteValue == .text("Hello, SQLite!")) } - @Test func testSQLiteRawValueToString() { + @Test func stringFromSQLiteValue() { #expect(String(SQLiteValue.text("Hello, SQLite!")) == "Hello, SQLite!") #expect(String(SQLiteValue.int(42)) == nil) - #expect(String(SQLiteValue.blob(Data([0x01, 0x02]))) == nil) + #expect(String(SQLiteValue.real(42)) == nil) + #expect(String(SQLiteValue.blob(Data([0x42]))) == nil) #expect(String(SQLiteValue.null) == nil) } } diff --git a/Tests/DataLiteCoreTests/Extensions/UUIDTests.swift b/Tests/DataLiteCoreTests/Extensions/UUIDTests.swift index 54bc0ba..194ee02 100644 --- a/Tests/DataLiteCoreTests/Extensions/UUIDTests.swift +++ b/Tests/DataLiteCoreTests/Extensions/UUIDTests.swift @@ -3,18 +3,19 @@ import Foundation import DataLiteCore struct UUIDTests { - @Test func testUUIDToSQLiteRawValue() { + @Test func uuidToSQLiteValue() { let uuid = UUID(uuidString: "123e4567-e89b-12d3-a456-426614174000")! #expect(uuid.sqliteValue == .text("123E4567-E89B-12D3-A456-426614174000")) } - @Test func testSQLiteRawValueToUUID() { + @Test func uuidFromSQLiteValue() { let raw = SQLiteValue.text("123e4567-e89b-12d3-a456-426614174000") #expect(UUID(raw) == UUID(uuidString: "123e4567-e89b-12d3-a456-426614174000")) - #expect(UUID(.text("invalid-uuid-string")) == nil) - #expect(UUID(.int(42)) == nil) - #expect(UUID(.blob(Data([0x01, 0x02]))) == nil) - #expect(UUID(.null) == nil) + #expect(UUID(SQLiteValue.int(42)) == nil) + #expect(UUID(SQLiteValue.real(42)) == nil) + #expect(UUID(SQLiteValue.text("42")) == nil) + #expect(UUID(SQLiteValue.blob(Data([0x42]))) == nil) + #expect(UUID(SQLiteValue.null) == nil) } } diff --git a/Tests/DataLiteCoreTests/Protocols/ArgumentsProtocolTests.swift b/Tests/DataLiteCoreTests/Protocols/ArgumentsProtocolTests.swift new file mode 100644 index 0000000..cbd2311 --- /dev/null +++ b/Tests/DataLiteCoreTests/Protocols/ArgumentsProtocolTests.swift @@ -0,0 +1,46 @@ +import Foundation +import Testing +import DataLiteCore + +struct ArgumentsProtocolTests { + @Test func typedSubscript() { + let arguments: Arguments = [ + .text("one"), + .text("two"), + .int(42) + ] + + #expect(arguments[0] == TestModel.one) + #expect(arguments[1] == TestModel.two) + #expect(arguments[2] as TestModel? == nil) + } +} + +private extension ArgumentsProtocolTests { + enum TestModel: String, SQLiteRepresentable { + case one, two + } + + struct Arguments: ArgumentsProtocol, ExpressibleByArrayLiteral { + private let values: [SQLiteValue] + + var startIndex: Int { values.startIndex } + var endIndex: Int { values.endIndex } + + init(_ values: [SQLiteValue]) { + self.values = values + } + + init(arrayLiteral elements: SQLiteValue...) { + self.values = elements + } + + subscript(index: Int) -> SQLiteValue { + values[index] + } + + func index(after i: Int) -> Int { + values.index(after: i) + } + } +} diff --git a/Tests/DataLiteCoreTests/Protocols/SQLiteBindableTests.swift b/Tests/DataLiteCoreTests/Protocols/SQLiteBindableTests.swift index cb6a6ce..a9d9cdb 100644 --- a/Tests/DataLiteCoreTests/Protocols/SQLiteBindableTests.swift +++ b/Tests/DataLiteCoreTests/Protocols/SQLiteBindableTests.swift @@ -2,11 +2,6 @@ import Foundation import Testing import DataLiteCore -private struct BindableStub: SQLiteBindable { - let value: SQLiteValue - var sqliteValue: SQLiteValue { value } -} - struct SQLiteBindableTests { @Test(arguments: [ SQLiteValue.int(42), @@ -15,8 +10,15 @@ struct SQLiteBindableTests { SQLiteValue.blob(Data([0x00, 0xAB])), SQLiteValue.null ]) - func testDefaultSqliteLiteralPassThrough(_ value: SQLiteValue) { - let stub = BindableStub(value: value) + func sqliteLiteral(_ value: SQLiteValue) { + let stub = Bindable(value: value) #expect(stub.sqliteLiteral == value.sqliteLiteral) } } + +private extension SQLiteBindableTests { + struct Bindable: SQLiteBindable { + let value: SQLiteValue + var sqliteValue: SQLiteValue { value } + } +} diff --git a/Tests/DataLiteCoreTests/Resources/empty_script.sql b/Tests/DataLiteCoreTests/Resources/empty_script.sql deleted file mode 100644 index 8b13789..0000000 --- a/Tests/DataLiteCoreTests/Resources/empty_script.sql +++ /dev/null @@ -1 +0,0 @@ - diff --git a/Tests/DataLiteCoreTests/Resources/invalid_script.sql b/Tests/DataLiteCoreTests/Resources/invalid_script.sql deleted file mode 100644 index 38c4d56dcb354331bca0884e8af92c13dd44cd97..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 793 zcmWIWW@h1H;9y{2I8YN738dgah(UoNGp{T$Co?6!IJqdZphT~@FefyGmw|od9PxC8 zx#HSI0q$0kES%!=rGmH!}luj<4rm{?Bno3ma z?_CwsmOR__Wcu@`Pt=z!S++xE+LER(KCD8jJUpv*d~HczBDQpw=+oz7&3D*7&E;io z)^Ms#%uEVQit|j9yRgSie%j4{$2!jiRiAmX=gs4~XPXSS%(?w3@p<&Xq}j{>(|TU2ywru#e6e9$w!`rDx9w2bcQloDC|Sd`2gz^enH= z<(^*MNxf%!gE?NJz}dP0P#3n>i;VQ+CdbX%SO0b00lP%gD-nx#sng7f)7Y zCq2tp@;c+qqh+rjy~ug^Dk~{3D=Fy|Bwoc$V#0EP5e*2bKVqG=f+J uA{T2qVq_9wMx-2MyFn=j29`7?0GSk}qX2JKHjr^lKzJHRTL8VszyJVR9Q(Ea diff --git a/Tests/DataLiteCoreTests/Resources/valid_script.sql b/Tests/DataLiteCoreTests/Resources/valid_script.sql deleted file mode 100644 index 9e7a6d9..0000000 --- a/Tests/DataLiteCoreTests/Resources/valid_script.sql +++ /dev/null @@ -1,16 +0,0 @@ - -- This is a single-line comment. -CREATE TABLE users ( - id INTEGER PRIMARY KEY, - username TEXT NOT NULL, - email TEXT NOT NULL -); - -/* - This is a multi-line comment. - It spans multiple lines and can contain any text. -*/ -INSERT INTO users (id, username, email) -VALUES - (1, 'john_doe', 'john@example.com'), -- Inserting John Doe - /* This is a comment inside a statement */ - (2, 'jane_doe', 'jane@example.com'); -- Inserting Jane Doe diff --git a/Tests/DataLiteCoreTests/Structures/PragmaTests.swift b/Tests/DataLiteCoreTests/Structures/PragmaTests.swift new file mode 100644 index 0000000..78fb4fc --- /dev/null +++ b/Tests/DataLiteCoreTests/Structures/PragmaTests.swift @@ -0,0 +1,30 @@ +import Testing + +@testable import DataLiteCore + +struct PragmaTests { + @Test(arguments: [ + (Pragma.applicationID, "application_id"), + (Pragma.foreignKeys, "foreign_keys"), + (Pragma.journalMode, "journal_mode"), + (Pragma.synchronous, "synchronous"), + (Pragma.userVersion, "user_version"), + (Pragma.busyTimeout, "busy_timeout") + ]) + func predefinedPragmas(_ pragma: Pragma, _ expected: String) { + #expect(pragma.rawValue == expected) + #expect(pragma.description == expected) + } + + @Test func initRawValue() { + let pragma = Pragma(rawValue: "custom_pragma") + #expect(pragma.rawValue == "custom_pragma") + #expect(pragma.description == "custom_pragma") + } + + @Test func initStringLiteral() { + let pragma: Pragma = "another_pragma" + #expect(pragma.rawValue == "another_pragma") + #expect(pragma.description == "another_pragma") + } +} diff --git a/Tests/DataLiteCoreTests/Structures/SQLiteErrorTests.swift b/Tests/DataLiteCoreTests/Structures/SQLiteErrorTests.swift index faa9231..68c9950 100644 --- a/Tests/DataLiteCoreTests/Structures/SQLiteErrorTests.swift +++ b/Tests/DataLiteCoreTests/Structures/SQLiteErrorTests.swift @@ -1,28 +1,36 @@ -import Foundation import Testing import DataLiteC + @testable import DataLiteCore struct SQLiteErrorTests { - @Test func testInitWithConnection() { - var db: OpaquePointer? = nil - defer { sqlite3_close(db) } - sqlite3_open(":memory:", &db) - sqlite3_exec(db, "INVALID SQL", nil, nil, nil) + @Test func initWithConnection() { + var connection: OpaquePointer! + defer { sqlite3_close(connection) } + sqlite3_open(":memory:", &connection) + sqlite3_exec(connection, "INVALID SQL", nil, nil, nil) - let error = SQLiteError(db!) + let error = SQLiteError(connection) #expect(error.code == SQLITE_ERROR) #expect(error.message == "near \"INVALID\": syntax error") } - @Test func testInitWithCodeAndMessage() { + @Test func initWithCodeAndMessage() { let error = SQLiteError(code: 1, message: "Test Error Message") #expect(error.code == 1) #expect(error.message == "Test Error Message") } - @Test func testDescription() { + @Test func description() { let error = SQLiteError(code: 1, message: "Test Error Message") - #expect(error.description == "SQLiteError code: 1 message: Test Error Message") + #expect(error.description == "SQLiteError(1): Test Error Message") + } + + @Test func equality() { + let lhs = SQLiteError(code: 1, message: "First") + let rhs = SQLiteError(code: 1, message: "First") + let different = SQLiteError(code: 2, message: "Second") + #expect(lhs == rhs) + #expect(lhs != different) } } diff --git a/Tests/DataLiteCoreTests/Structures/SQLiteRowTests.swift b/Tests/DataLiteCoreTests/Structures/SQLiteRowTests.swift index 5f8f4c5..fa2fca2 100644 --- a/Tests/DataLiteCoreTests/Structures/SQLiteRowTests.swift +++ b/Tests/DataLiteCoreTests/Structures/SQLiteRowTests.swift @@ -1,91 +1,106 @@ -import XCTest -import DataLiteCore +import Testing -final class SQLiteRowTests: XCTestCase { - func testInitEmptyRow() { - let row = SQLiteRow() - XCTAssertTrue(row.isEmpty) - XCTAssertEqual(row.count, 0) - } - - func testUpdateColumnPosition() { +@testable import DataLiteCore + +struct SQLiteRowTests { + @Test func subscriptByColumn() { var row = SQLiteRow() + #expect(row["name"] == nil) + row["name"] = .text("Alice") - row["age"] = .int(30) + #expect(row["name"] == .text("Alice")) row["name"] = .text("Bob") - - XCTAssertEqual(row[0].column, "name") - XCTAssertEqual(row[0].value, .text("Bob")) - } - - func testSubscriptByColumn() { - var row = SQLiteRow() - row["name"] = .text("Alice") - - XCTAssertEqual(row["name"], .text("Alice")) - XCTAssertNil(row["age"]) - - row["age"] = SQLiteValue.int(30) - XCTAssertEqual(row["age"], .int(30)) - } - - func testSubscriptByIndex() { - var row = SQLiteRow() - row["name"] = .text("Alice") - row["age"] = .int(30) - - let firstElement = row[row.startIndex] - XCTAssertEqual(firstElement.column, "name") - XCTAssertEqual(firstElement.value, .text("Alice")) - - let secondElement = row[row.index(after: row.startIndex)] - XCTAssertEqual(secondElement.column, "age") - XCTAssertEqual(secondElement.value, .int(30)) - } - - func testDescription() { - var row = SQLiteRow() - row["name"] = .text("Alice") - row["age"] = .int(30) - - let expectedDescription = #"["name": 'Alice', "age": 30]"# - XCTAssertEqual(row.description, expectedDescription) - } - - func testCountAndIsEmpty() { - var row = SQLiteRow() - XCTAssertTrue(row.isEmpty) - XCTAssertEqual(row.count, 0) - - row["name"] = .text("Alice") - XCTAssertFalse(row.isEmpty) - XCTAssertEqual(row.count, 1) - - row["age"] = .int(30) - XCTAssertEqual(row.count, 2) + #expect(row["name"] == .text("Bob")) row["name"] = nil - XCTAssertEqual(row.count, 1) + #expect(row["name"] == nil) } - func testIteration() { + @Test func subscriptByIndex() { + let row: SQLiteRow = [ + "name": .text("Alice"), + "age": .int(30), + "city": .text("Wonderland") + ] + #expect(row[0] == ("name", .text("Alice"))) + #expect(row[1] == ("age", .int(30))) + #expect(row[2] == ("city", .text("Wonderland"))) + } + + @Test func columns() { + let row: SQLiteRow = [ + "name": .text("Alice"), + "age": .int(30), + "city": .text("Wonderland") + ] + #expect(row.columns == ["name", "age", "city"]) + } + + @Test func namedParameters() { + let row: SQLiteRow = [ + "name": .text("Alice"), + "age": .int(30), + "city": .text("Wonderland") + ] + #expect(row.namedParameters == [":name", ":age", ":city"]) + } + + @Test func containsColumn() { + let row: SQLiteRow = ["one": .null] + #expect(row.contains("one")) + #expect(row.contains("two") == false) + } + + @Test func description() { + let row: SQLiteRow = [ + "name": .text("Alice"), + "age": .int(30) + ] + #expect(row.description == #"["name": 'Alice', "age": 30]"#) + } + + @Test func startIndex() { + let row: SQLiteRow = [ + "name": .text("Alice"), + "age": .int(30) + ] + #expect(row.startIndex == 0) + } + + @Test func endIndex() { + let row: SQLiteRow = [ + "name": .text("Alice"), + "age": .int(30) + ] + #expect(row.endIndex == 2) + } + + @Test func endIndexEmptyRow() { + let row = SQLiteRow() + #expect(row.endIndex == 0) + } + + @Test func isEmpty() { var row = SQLiteRow() - row["name"] = .text("Alice") - row["age"] = .int(30) - row["city"] = .text("Wonderland") + #expect(row.isEmpty) - var elements: [SQLiteRow.Element] = [] - for (column, value) in row { - elements.append((column, value)) - } + row["one"] = .int(1) + #expect(row.isEmpty == false) + } + + @Test func count() { + var row = SQLiteRow() + #expect(row.count == 0) - XCTAssertEqual(elements.count, 3) - XCTAssertEqual(elements[0].column, "name") - XCTAssertEqual(elements[0].value, .text("Alice")) - XCTAssertEqual(elements[1].column, "age") - XCTAssertEqual(elements[1].value, .int(30)) - XCTAssertEqual(elements[2].column, "city") - XCTAssertEqual(elements[2].value, .text("Wonderland")) + row["one"] = .int(1) + #expect(row.count == 1) + } + + @Test func indexAfter() { + let row = SQLiteRow() + #expect(row.index(after: 0) == 1) + #expect(row.index(after: 1) == 2) + #expect(row.index(after: 2) == 3) } }