333 lines
13 KiB
Swift
333 lines
13 KiB
Swift
import XCTest
|
|
import GRDB
|
|
|
|
class DatabaseCursorTests: GRDBTestCase {
|
|
|
|
// TODO: this test should be duplicated for all cursor types
|
|
func testNextReturnsNilAfterExhaustion() throws {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
do {
|
|
let cursor = try Int.fetchCursor(db, sql: "SELECT 1 WHERE 0")
|
|
XCTAssert(try cursor.next() == nil) // end
|
|
XCTAssert(try cursor.next() == nil) // past the end
|
|
}
|
|
do {
|
|
let cursor = try Int.fetchCursor(db, sql: "SELECT 1")
|
|
XCTAssertEqual(try cursor.next()!, 1)
|
|
XCTAssert(try cursor.next() == nil) // end
|
|
XCTAssert(try cursor.next() == nil) // past the end
|
|
}
|
|
do {
|
|
let cursor = try Int.fetchCursor(db, sql: "SELECT 1 UNION ALL SELECT 2")
|
|
XCTAssertEqual(try cursor.next()!, 1)
|
|
XCTAssertEqual(try cursor.next()!, 2)
|
|
XCTAssert(try cursor.next() == nil) // end
|
|
XCTAssert(try cursor.next() == nil) // past the end
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: this test should be duplicated for all cursor types
|
|
func testNextFollowedByForEach() throws {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
do {
|
|
let cursor = try Int.fetchCursor(db, sql: "SELECT 1")
|
|
XCTAssertEqual(try cursor.next()!, 1)
|
|
try cursor.forEach { _ in
|
|
XCTFail("Cursor iteration should have ended")
|
|
}
|
|
XCTAssert(try cursor.next() == nil) // end
|
|
}
|
|
do {
|
|
let cursor = try Int.fetchCursor(db, sql: "SELECT 1 UNION SELECT 2")
|
|
XCTAssertEqual(try cursor.next()!, 1)
|
|
try cursor.forEach { i in
|
|
XCTAssertEqual(i, 2)
|
|
}
|
|
XCTAssert(try cursor.next() == nil) // end
|
|
}
|
|
do {
|
|
let cursor = try Int.fetchCursor(db, sql: "SELECT 1 UNION SELECT 2 UNION SELECT 3")
|
|
XCTAssertEqual(try cursor.next()!, 1)
|
|
try XCTAssertEqual(Array(cursor), [2, 3])
|
|
XCTAssert(try cursor.next() == nil) // end
|
|
}
|
|
do {
|
|
struct Break: Error { }
|
|
let cursor = try Int.fetchCursor(db, sql: "SELECT 1 UNION SELECT 2 UNION SELECT 3")
|
|
try? cursor.forEach { i in
|
|
XCTAssertEqual(i, 1)
|
|
throw Break()
|
|
}
|
|
try? cursor.forEach { i in
|
|
XCTAssertEqual(i, 2)
|
|
throw Break()
|
|
}
|
|
try? cursor.forEach { i in
|
|
XCTAssertEqual(i, 3)
|
|
throw Break()
|
|
}
|
|
XCTAssert(try cursor.next() == nil) // end
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: this test should be duplicated for all cursor types
|
|
func testStepError() throws {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
let customError = NSError(domain: "Custom", code: 0xDEAD)
|
|
try dbQueue.inDatabase { db in
|
|
db.add(function: DatabaseFunction("throw", argumentCount: 0, pure: true) { _ in throw customError })
|
|
let cursor = try Int.fetchCursor(db, sql: "SELECT throw()")
|
|
do {
|
|
_ = try cursor.next()
|
|
XCTFail()
|
|
} catch let error as DatabaseError {
|
|
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
|
|
XCTAssertEqual(error.message, "\(customError)")
|
|
XCTAssertEqual(error.sql!, "SELECT throw()")
|
|
XCTAssertEqual(error.description, "SQLite error 1: \(customError) - while executing `SELECT throw()`")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: this test should be duplicated for all cursor types
|
|
func testStepDatabaseError() throws {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
let customError = DatabaseError(resultCode: ResultCode(rawValue: 0xDEAD), message: "custom error")
|
|
try dbQueue.inDatabase { db in
|
|
db.add(function: DatabaseFunction("throw", argumentCount: 0, pure: true) { _ in throw customError })
|
|
let cursor = try Int.fetchCursor(db, sql: "SELECT throw()")
|
|
do {
|
|
_ = try cursor.next()
|
|
XCTFail()
|
|
} catch let error as DatabaseError {
|
|
XCTAssertEqual(error.resultCode.rawValue, 0xAD)
|
|
XCTAssertEqual(error.extendedResultCode.rawValue, 0xDEAD)
|
|
XCTAssertEqual(error.message, "custom error")
|
|
XCTAssertEqual(error.sql!, "SELECT throw()")
|
|
XCTAssertEqual(error.description, "SQLite error 173: custom error - while executing `SELECT throw()`")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Regression test for http://github.com/groue/GRDB.swift/issues/583
|
|
func testIssue583() throws {
|
|
struct User: Codable, TableRecord, FetchableRecord, MutablePersistableRecord {
|
|
static let databaseTableName: String = "user"
|
|
|
|
var id: Int64?
|
|
var username: String
|
|
var isFlagged: Bool
|
|
|
|
init(id: Int64? = nil, username: String, isFlagged: Bool = false) {
|
|
self.id = id
|
|
self.username = username
|
|
self.isFlagged = isFlagged
|
|
}
|
|
|
|
mutating func didInsert(_ inserted: InsertionSuccess) {
|
|
id = inserted.rowID
|
|
}
|
|
}
|
|
|
|
struct FlagUser: Codable, TableRecord, FetchableRecord, MutablePersistableRecord {
|
|
static let databaseTableName: String = "flagUser"
|
|
|
|
var username: String
|
|
}
|
|
|
|
let queue = try makeDatabaseQueue()
|
|
try queue.write { database in
|
|
try database.create(table: User.databaseTableName) { definition in
|
|
definition.column("id", .integer).primaryKey(autoincrement: true)
|
|
definition.column("username", .text).notNull()
|
|
definition.column("isFlagged", .boolean).notNull().defaults(to: false)
|
|
}
|
|
|
|
try database.create(table: FlagUser.databaseTableName) { definition in
|
|
definition.column("username", .text).notNull()
|
|
}
|
|
|
|
try [Int](0...50).forEach {
|
|
var user = User(username: "User\($0)")
|
|
try user.insert(database)
|
|
}
|
|
|
|
try [Int](40...60).forEach {
|
|
var flag = FlagUser(username: "User\($0)")
|
|
try flag.insert(database)
|
|
}
|
|
}
|
|
|
|
let query = "SELECT * FROM flagUser WHERE (SELECT COUNT(id) FROM user WHERE username = flagUser.username AND isFlagged = 1) = 0"
|
|
try queue.inDatabase { database in
|
|
let cursor = try FlagUser.fetchCursor(database, sql: query)
|
|
while let flagged = try cursor.next() {
|
|
_ = try User.fetchOne(database, sql: "SELECT * FROM user WHERE username = '\(flagged.username)' LIMIT 1") ??
|
|
User(username: flagged.username)
|
|
}
|
|
}
|
|
|
|
// For the record, the lines below show how this test used to fail,
|
|
// with raw C SQLite3 apis. The faulty line is the call to
|
|
// sqlite3_set_authorizer during the statement iteration.
|
|
|
|
// if #available(OSX 10.14, *) {
|
|
// var connection: SQLiteConnection? = nil
|
|
// sqlite3_open_v2(":memory:", &connection, SQLITE_OPEN_READWRITE | SQLITE_OPEN_NOMUTEX, nil)
|
|
// sqlite3_extended_result_codes(connection, 1)
|
|
//
|
|
// sqlite3_exec(connection, """
|
|
// CREATE TABLE user (username TEXT NOT NULL);
|
|
// CREATE TABLE flagUser (username TEXT NOT NULL);
|
|
// INSERT INTO flagUser (username) VALUES ('User1');
|
|
// INSERT INTO flagUser (username) VALUES ('User2');
|
|
// """, nil, nil, nil)
|
|
//
|
|
// var statement: SQLiteStatement? = nil
|
|
// sqlite3_set_authorizer(connection, { (_, _, _, _, _, _) in SQLITE_OK }, nil)
|
|
// sqlite3_prepare_v3(connection, """
|
|
// SELECT * FROM flagUser WHERE (SELECT COUNT(*) FROM user WHERE username = flagUser.username) = 0
|
|
// """, -1, 0, &statement, nil)
|
|
// sqlite3_set_authorizer(connection, nil, nil)
|
|
// while true {
|
|
// let code = sqlite3_step(statement)
|
|
// if code == SQLITE_DONE {
|
|
// break
|
|
// } else if code == SQLITE_ROW {
|
|
// // part of the compilation of another statement, here
|
|
// // reduced to the strict minimum that reproduces
|
|
// // the error.
|
|
// sqlite3_set_authorizer(connection, nil, nil)
|
|
// } else {
|
|
// print(String(cString: sqlite3_errmsg(connection)))
|
|
// XCTFail("Error \(code)")
|
|
// break
|
|
// }
|
|
// }
|
|
// sqlite3_finalize(statement)
|
|
// sqlite3_close_v2(connection)
|
|
// }
|
|
}
|
|
|
|
// For profiling tests
|
|
let profilingSQL = """
|
|
WITH RECURSIVE
|
|
cnt(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM cnt WHERE x<10000000)
|
|
SELECT x FROM cnt
|
|
"""
|
|
|
|
// Profiling test
|
|
func testRowCursorStep() throws {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.read { db in
|
|
let cursor: RowCursor = try Row.fetchCursor(db, sql: profilingSQL)
|
|
while let _ = try cursor.next() { }
|
|
}
|
|
}
|
|
|
|
// Profiling test
|
|
func testRowCursorForEach() throws {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.read { db in
|
|
let cursor: RowCursor = try Row.fetchCursor(db, sql: profilingSQL)
|
|
try cursor.forEach { _ in }
|
|
}
|
|
}
|
|
|
|
// Profiling test
|
|
func testDatabaseValueCursorStep() throws {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.read { db in
|
|
let cursor: DatabaseValueCursor<Int> = try Int.fetchCursor(db, sql: profilingSQL)
|
|
while let _ = try cursor.next() { }
|
|
}
|
|
}
|
|
|
|
// Profiling test
|
|
func testDatabaseValueCursorForEach() throws {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.read { db in
|
|
let cursor: DatabaseValueCursor<Int> = try Int.fetchCursor(db, sql: profilingSQL)
|
|
try cursor.forEach { _ in }
|
|
}
|
|
}
|
|
|
|
// Profiling test
|
|
func testNullableDatabaseValueCursorStep() throws {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.read { db in
|
|
let cursor: DatabaseValueCursor<Int?> = try Optional<Int>.fetchCursor(db, sql: profilingSQL)
|
|
while let _ = try cursor.next() { }
|
|
}
|
|
}
|
|
|
|
// Profiling test
|
|
func testNullableDatabaseValueCursorForEach() throws {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.read { db in
|
|
let cursor: DatabaseValueCursor<Int?> = try Optional<Int>.fetchCursor(db, sql: profilingSQL)
|
|
try cursor.forEach { _ in }
|
|
}
|
|
}
|
|
|
|
// Profiling test
|
|
func testFastDatabaseValueCursorStep() throws {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.read { db in
|
|
let cursor: FastDatabaseValueCursor<Int> = try Int.fetchCursor(db, sql: profilingSQL)
|
|
while let _ = try cursor.next() { }
|
|
}
|
|
}
|
|
|
|
// Profiling test
|
|
func testFastDatabaseValueCursorForEach() throws {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.read { db in
|
|
let cursor: FastDatabaseValueCursor<Int> = try Int.fetchCursor(db, sql: profilingSQL)
|
|
try cursor.forEach { _ in }
|
|
}
|
|
}
|
|
|
|
// Profiling test
|
|
func testFastNullableDatabaseValueCursorStep() throws {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.read { db in
|
|
let cursor: FastDatabaseValueCursor<Int?> = try Optional<Int>.fetchCursor(db, sql: profilingSQL)
|
|
while let _ = try cursor.next() { }
|
|
}
|
|
}
|
|
|
|
// Profiling test
|
|
func testFastNullableDatabaseValueCursorForEach() throws {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.read { db in
|
|
let cursor: FastDatabaseValueCursor<Int?> = try Optional<Int>.fetchCursor(db, sql: profilingSQL)
|
|
try cursor.forEach { _ in }
|
|
}
|
|
}
|
|
|
|
// Profiling test
|
|
func testRecordCursorStep() throws {
|
|
struct S: FetchableRecord { init(row: Row) { } }
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.read { db in
|
|
let cursor: RecordCursor<S> = try S.fetchCursor(db, sql: profilingSQL)
|
|
while let _ = try cursor.next() { }
|
|
}
|
|
}
|
|
|
|
// Profiling test
|
|
func testRecordCursorForEach() throws {
|
|
struct S: FetchableRecord { init(row: Row) { } }
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.read { db in
|
|
let cursor: RecordCursor<S> = try S.fetchCursor(db, sql: profilingSQL)
|
|
try cursor.forEach { _ in }
|
|
}
|
|
}
|
|
}
|